본문으로 바로가기

아무리 검색해도 관련 코드가 없었다..
StackOverFlow에서는 질문만 있고 답변이 없었다..

 

지푸라기라도 잡는 심정으로 Ai들에게 물어보았지만

Google Bard는 존재하지 않는 'vue-google-classroom'을 사용하라고 하고..(23.6.7 기준 비공개 상태)
ChatGPT 또한 동작하지 않는 코드를 뱉어주었다..

 

그래서 ChatGPT에게 예시코드를 Vuejs로 변경해달라고 요청하였더니 깔끔하게 테스트는 되었다..

실 사용할땐 많이 변경되겠지만, Google Bard는 자기가 싫어하는 부분은 짤라서 알려줬다..

정리가 아닌 메모하는 글이기 때문에 코드가 더 많습니다..

 

QuickStart Code [Javascript => Vue.js] (feat. ChatGPT)

<template>
  <div>
    <p>Classroom API Quickstart</p>

    <!-- Add buttons to initiate auth sequence and sign out -->
    <button ref="authorizeButton" @click="handleAuthClick">Authorize</button>
    <button ref="signoutButton" @click="handleSignoutClick">Sign Out</button>

    <pre id="content" style="white-space: pre-wrap;">{{ output }}</pre>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      output: '',
      CLIENT_ID: '<YOUR_CLIENT_ID>',
      API_KEY: '<YOUR_API_KEY>',
      DISCOVERY_DOC: 'https://classroom.googleapis.com/$discovery/rest',
      SCOPES: [
      	'https://www.googleapis.com/auth/classroom.courses.readonly'
      ],
      tokenClient: null,
      gapiInited: false,
      gisInited: false,
    }
  },
  mounted() {
    this.$refs.authorizeButton.style.visibility = 'hidden'
    this.$refs.signoutButton.style.visibility = 'hidden'

    // Load Google Identity Services script
    const gisScript = document.createElement('script')
    gisScript.src = 'https://accounts.google.com/gsi/client'
    gisScript.async = true
    gisScript.defer = true
    gisScript.onload = () => {
      this.gisLoaded()
    }
    document.body.appendChild(gisScript)

    // Load gapi.client script
    const gapiScript = document.createElement('script')
    gapiScript.src = 'https://apis.google.com/js/api.js'
    gapiScript.async = true
    gapiScript.defer = true
    gapiScript.onload = () => {
      this.gapiLoaded()
    }
    document.body.appendChild(gapiScript)
  },
  methods: {
    gapiLoaded() {
      gapi.load('client', () => {
        this.initializeGapiClient()
      })
    },
    async initializeGapiClient() {
      await gapi.client.init({
        apiKey: this.API_KEY,
        discoveryDocs: [this.DISCOVERY_DOC],
      })
      this.gapiInited = true
      this.maybeEnableButtons()
    },
    gisLoaded() {
      this.tokenClient = google.accounts.oauth2.initTokenClient({
        client_id: this.CLIENT_ID,
        scope: this.SCOPES.join(' '),
        callback: '', // defined later
      })
      this.gisInited = true
      this.maybeEnableButtons()
    },
    maybeEnableButtons() {
      if (this.gapiInited && this.gisInited) {
        this.$refs.authorizeButton.style.visibility = 'visible'
      }
    },
    handleAuthClick() {
      this.tokenClient.callback = async (resp) => {
        if (resp.error !== undefined) {
          throw resp
        }
        this.$refs.signoutButton.style.visibility = 'visible'
        this.$refs.authorizeButton.innerText = 'Refresh'
        await this.listCourses()
      }

      if (gapi.client.getToken() === null) {
        this.tokenClient.requestAccessToken({ prompt: 'consent' })
      } else {
        this.tokenClient.requestAccessToken({ prompt: '' })
      }
    },
    handleSignoutClick() {
      const token = gapi.client.getToken()
      if (token !== null) {
        google.accounts.oauth2.revoke(token.access_token)
        gapi.client.setToken('')
        this.output = ''
        this.$refs.authorizeButton.innerText = 'Authorize'
        this.$refs.signoutButton.style.visibility = 'hidden'
      }
    },
    async listCourses() {
      let response
      try {
        response = await gapi.client.classroom.courses.list({
          pageSize: 10,
        })
      } catch (err) {
        this.output = err.message
        return
      }

      const courses = response.result.courses
      if (!courses || courses.length === 0) {
        this.output = 'No courses found.'
        return
      }
      const output = courses.reduce((str, course) => `${str}${course.name}\n`, 'Courses:\n')
      this.output = output
    },
  },
}
</script>

<style>
</style>

클래스룸 List 개선(라고 쓰고 제가 쓸거 같은 형태로 변형) 버전

<template>
  <div class="w-screen h-screen flex flex-col items-center justify-center p-20 gap-y-4">
    <div class="w-[22rem] h-10 relative">
      <p class="w-fit font-semibold text-xl">Classroom API Quickstart</p>
      <div 
        class="absolute top-0 right-0"
      >
        <button 
          v-if="isInited && !isAuthorized"
          type="button"
          class="text-sm py-1 px-2 rounded-full bg-[#D9D9D9] hover:bg-[#ffe400] cursor-pointer"
          ref="authorizeButton" 
          @click="handleAuthClick"
        >
          Authorize
        </button>
        <button 
          v-if="isAuthorized"
          type="button"
          class="text-sm py-1 px-2 rounded-full bg-[#D9D9D9] hover:bg-[#ffe400] cursor-pointer"
          ref="signoutButton" 
          @click="handleSignoutClick"
        >
          Sign Out
        </button>
      </div>
    </div>
    
    <div class="w-full h-[25rem] flex items-center justify-center">
      <div 
        v-if="isAuthorized && courseList.length"
        class="w-fit h-full"
      >
        <div class="flex items-center border border-[#E5E5E5] rounded-t text-center">
          <div class="w-44 py-2 font-semibold bg-[#E5E5E5]">
            Name
          </div>
          <div class="w-44 py-2 font-semibold bg-[#E5E5E5]">
            State
          </div>
        </div>
        <div
          class="flex items-center border border-[#E5E5E5] text-center last:rounded-b-lg"
          v-for="(course, index) in courseList"
          :key="`${course.id}-${index}`"
        >
          <div class="w-44 py-2 border-x">
            {{ course.name }}
          </div>
          <div class="w-44 py-2">
            {{ course.courseState }}
          </div>
        </div>
      </div>
      <div 
        v-else
        class="whitespace-pre-wrap"
      >{{ output }}</div>
    </div>

    <div 
      class="w-full h-10 flex items-center justify-center"
    >
      <a 
        v-if="isAuthorized"
        class="!border border-l-0 border-[#E5E5E5] w-8 h-8 rounded-l bg-white flex justify-center items-center cursor-pointer mw:w-12 mw:h-12 group/lt"
        @click="changePage(-1)"
      >
        <svg
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <mask
            id="mask0_474_184"
            style="mask-type: alpha"
            maskUnits="userSpaceOnUse"
            x="0"
            y="0"
            width="24"
            height="24"
          >
            <rect width="24" height="24" fill="#D9D9D9" />
          </mask>
          <g mask="url(#mask0_474_184)">
            <path
              d="M14 18L8 12L14 6L15.4 7.4L10.8 12L15.4 16.6L14 18Z"
              class="fill-[#D9D9D9] group-hover/lt:fill-[#666]"
            />
          </g>
        </svg>
      </a>
      <a 
        v-if="isAuthorized"
        class="!border border-l-0 border-[#E5E5E5] w-8 h-8 rounded-r bg-white flex justify-center items-center cursor-pointer mw:w-12 mw:h-12 group/gt"
        @click="changePage(1)"
      >
        <svg
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <mask
            id="mask0_474_210"
            style="mask-type: alpha"
            maskUnits="userSpaceOnUse"
            x="0"
            y="0"
            width="24"
            height="24"
          >
            <rect width="24" height="24" fill="#D9D9D9" />
          </mask>
          <g mask="url(#mask0_474_210)">
            <path
              d="M9.4 18L8 16.6L12.6 12L8 7.4L9.4 6L15.4 12L9.4 18Z"
              class="fill-[#D9D9D9] group-hover/gt:fill-[#666]"
            />
          </g>
        </svg>
      </a>
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    isInited() {
      return this.gapiInited && this.gisInited
    },
  },
  data() {
    return {
      CLIENT_ID: '<YOUR_CLIENT_ID>',
      API_KEY: '<YOUR_API_KEY>',
      DISCOVERY_DOC: 'https://classroom.googleapis.com/$discovery/rest',
      SCOPES: [
        'https://www.googleapis.com/auth/classroom.courses.readonly',
        'https://www.googleapis.com/auth/classroom.coursework.students',
        'https://www.googleapis.com/auth/classroom.coursework.me',
        'https://www.googleapis.com/auth/classroom.courseworkmaterials',
        'https://www.googleapis.com/auth/classroom.rosters.readonly',
      ],
      output: '',
      tokenClient: null,
      isAuthorized: false,
      gapiInited: false,
      gisInited: false,
      courseList: [],
      pageToken: null,
      tokenStorage: [null], // pageToken 저장소
      pageIndex: 0,
      pageSize: 10,
    }
  },
  mounted() {
    const scripts = [
      {
        src: 'https://accounts.google.com/gsi/client',
        $function: 'gisLoaded',
      },
      {
        src: 'https://apis.google.com/js/api.js',
        $function: 'gapiLoaded',
      },
    ]

    for (let obj of scripts) {
      this.setScript(obj)
    }
  },
  methods: {
    isExistedScript(script) { // 스크립트 존재 여부 판별
      return !!document?.querySelector(`script[src="${script}"]`)
    },
    setScript({src, $function}) { // 스크립트 등록 및 init method 호출
      if (!this.isExistedScript(src)) { // 이미 스크립트가 document 내에 존재한다면 굳이 추가로 삽입할 필요 없을 것 같아서 추가함
        const script = document.createElement('script')
        script.src = src
        script.async = true
        script.defer = true
        script.onload = () => {
          this[$function]()
        }
        document.body.appendChild(script)
      } else {
        this[$function]()
      }
    },
    gapiLoaded() {
      gapi.load('client', () => {
        this.initializeGapiClient()
      })
    },
    async initializeGapiClient() {
      await gapi.client.init({
        apiKey: this.API_KEY,
        discoveryDocs: [this.DISCOVERY_DOC],
      })
      this.gapiInited = true
    },
    gisLoaded() {
      this.tokenClient = google.accounts.oauth2.initTokenClient({
        client_id: this.CLIENT_ID,
        scope: this.SCOPES.join(' '),	// scope 배열로 여러 개 넣을 경우 필수!
        callback: '', // defined later
      })
      this.gisInited = true
    },
    handleAuthClick() { // checking auth
      this.tokenClient.callback = async (resp) => {
        if (resp.error !== undefined) {
          throw resp
        }
        this.isAuthorized = true
        await this.listCourses()
      }

      if (gapi.client.getToken() === null) {
        this.tokenClient.requestAccessToken({ prompt: 'consent' })
      } else {
        this.tokenClient.requestAccessToken({ prompt: '' })
      }
    },
    handleSignoutClick() {  // Logout
      const token = gapi.client.getToken()
      if (token !== null) {
        google.accounts.oauth2.revoke(token.access_token)
        gapi.client.setToken('')
        this.output = ''
        this.isAuthorized = false
        this.courseList = []
      }
    },
    async listCourses() { // Get Classroom List
      this.output = ''
      let response
      try {
        response = await gapi.client.classroom.courses.list({
          pageSize: this.pageSize,
          pageToken: this.pageToken,
        })
        const result = response.result
        this.courseList = result.courses
        if (result.nextPageToken && !this.tokenStorage[this.pageIndex]) {
          this.tokenStorage.push(result.nextPageToken)
        }
        console.log(result, 'result')
        console.log(this.tokenStorage, 'tokenStorage')
      } catch (err) {
        this.output = err.message
        return
      }
    },
    async changePage(num) { // 페이지 변경. (앞, 뒤만 지원)
      const index = this.pageIndex + num
      if (index < 0 || index > this.tokenStorage.length - 1) {
        return
      }
      this.courseList = []
      this.pageToken = this.tokenStorage[index]
      this.pageIndex += num

      await this.listCourses()
    },
  },
}
</script>

<style>
</style>

 

Vue에서 일부 제작하다가 실패한 버전..

아무리 작업을 시도해도 API가 동작하지 않아서 일단 Vue로 제작하는 부분은 중단하였다..

수업 및 학생 목록 호출 OK
과제 등록 OK

과제 채점 및 과제 응답 상태 변경 Fail

 

index.vue

<template>
  <div class="w-screen h-screen flex flex-col items-center p-20 gap-y-4">
    <div class="w-[22rem] h-10 relative">
      <p class="w-fit font-semibold text-xl">Classroom API Quickstart</p>
      <div 
        class="absolute top-0 right-0"
      >
        <button 
          v-if="isInited && !isAuthorized"
          type="button"
          class="text-sm py-1 px-2 rounded-full bg-[#D9D9D9] hover:bg-[#ffe400] cursor-pointer"
          ref="authorizeButton" 
          @click="handleAuthClick"
        >
          Authorize
        </button>
        <button 
          v-if="isAuthorized"
          type="button"
          class="text-sm py-1 px-2 rounded-full bg-[#D9D9D9] hover:bg-[#ffe400] cursor-pointer"
          ref="signoutButton" 
          @click="handleSignoutClick"
        >
          Sign Out
        </button>
      </div>
    </div>
    
    <div v-if="isAuthorized">
      <student-list 
        v-if="classroomId && currentMode === 'student'"
        :work-info="workInfo"
        :classroom-id="classroomId"
        :set-classroom-id="setClassroomId"
      />
      <course-work-form 
        v-else-if="classroomId && currentMode === 'form'"
        :classroom-id="classroomId"
        :set-work-info="setWorkInfo"
        :set-classroom-id="setClassroomId"
      />
      <classroom-list 
        v-else
        :set-classroom-id="setClassroomId"
      />
    </div>
  </div>
</template>

<script>
import ClassroomList from './ClassroomList.vue'
import StudentList from './StudentList.vue'
import CourseWorkForm from './CourseWorkForm.vue'

export default {
  components: {
    ClassroomList,
    StudentList,
    CourseWorkForm,
  },
  computed: {
    isInited() {
      return this.gapiInited && this.gisInited
    },
  },
  data() {
    return {
      // CLIENT_ID: '<YOUR_CLIENT_ID>',
      // API_KEY: '<YOUR_API_KEY>',
      CLIENT_ID: '765323659219-m5k4tisotu2q92i1n6pbmv4dajpuisoh.apps.googleusercontent.com',
      API_KEY: 'AIzaSyCDeYwNA3fRKpP4IzYenonJwW8f6TThEg0',
      DISCOVERY_DOC: 'https://classroom.googleapis.com/$discovery/rest',
      SCOPES: [
        'https://www.googleapis.com/auth/classroom.courses.readonly',
        'https://www.googleapis.com/auth/classroom.coursework.students',
        'https://www.googleapis.com/auth/classroom.coursework.me',
        'https://www.googleapis.com/auth/classroom.courseworkmaterials',
        'https://www.googleapis.com/auth/classroom.rosters.readonly',
      ],
      tokenClient: null,
      isAuthorized: false,
      gapiInited: false,
      gisInited: false,
      classroomId: null,
      workInfo: null,
      currentMode: null,
    }
  },
  mounted() {
    const scripts = [
      {
        src: 'https://accounts.google.com/gsi/client',
        $function: 'gisLoaded',
      },
      {
        src: 'https://apis.google.com/js/api.js',
        $function: 'gapiLoaded',
      },
    ]

    for (let obj of scripts) {
      this.setScript(obj)
    }
  },
  methods: {
    isExistedScript(script) { // 스크립트 존재 여부 판별
      return !!document?.querySelector(`script[src="${script}"]`)
    },
    setScript({src, $function}) { // 스크립트 등록 및 init method 호출
      if (!this.isExistedScript(src)) { // 이미 스크립트가 document 내에 존재한다면 굳이 추가로 삽입할 필요 없을 것 같아서 추가함
        const script = document.createElement('script')
        script.src = src
        script.async = true
        script.defer = true
        script.onload = () => {
          this[$function]()
        }
        document.body.appendChild(script)
      } else {
        this[$function]()
      }
    },
    gapiLoaded() {
      gapi.load('client', () => {
        this.initializeGapiClient()
      })
    },
    async initializeGapiClient() {
      await gapi.client.init({
        apiKey: this.API_KEY,
        discoveryDocs: [this.DISCOVERY_DOC],
      })
      this.gapiInited = true
    },
    gisLoaded() {
      this.tokenClient = google.accounts.oauth2.initTokenClient({
        client_id: this.CLIENT_ID,
        scope: this.SCOPES.join(' '), // scope 배열로 여러 개 넣을 경우 필수!
        callback: '', // defined later
      })
      this.gisInited = true
    },
    handleAuthClick() { // checking auth
      this.tokenClient.callback = async (resp) => {
        console.log(resp, 'resp')
        if (resp.error !== undefined) {
          throw resp
        }
        this.isAuthorized = true
        // await this.listCourses()
      }

      if (gapi.client.getToken() === null) {
        this.tokenClient.requestAccessToken({ prompt: 'consent' })
      } else {
        this.tokenClient.requestAccessToken({ prompt: '' })
      }
    },
    handleSignoutClick() {  // Logout
      const token = gapi.client.getToken()
      if (token !== null) {
        google.accounts.oauth2.revoke(token.access_token)
        gapi.client.setToken('')
        // this.output = ''
        this.isAuthorized = false
        // this.courseList = []
      }
    },
    setClassroomId(id, mode) {
      if (typeof id !== 'string') id = null
      if (this.classroomId && id !== this.classroomId) {
        this.setWorkInfo(null)
      }
      this.classroomId = id
      this.currentMode = mode
    },
    setWorkInfo(obj) {
      this.workInfo = obj
    },
  },
}
</script>

<style>
</style>

 

ClassroomList.vue

<template>
  <div 
    class="w-full h-[25rem] flex items-center justify-center"
    v-if="isLoaded"
  >
    <div 
      v-if="courses.length"
      class="w-fit h-full"
    >
      <div class="flex items-center border border-[#E5E5E5] rounded-t text-center">
        <div class="w-44 py-2 font-semibold bg-[#E5E5E5]">
          Name
        </div>
        <div class="w-44 py-2 font-semibold bg-[#E5E5E5]">
          courseWork
        </div>
      </div>
      <div
        class="flex items-center border border-[#E5E5E5] text-center last:rounded-b-lg"
        v-for="(course, index) in courses"
        :key="`${course.id}-${index}`"
      >
        <div 
          class="w-44 py-2 border-r cursor-pointer hover:text-blue-700 hover:underline"
          @click="setClassroomId(course.id, 'student')"
        >
          {{ course.name }}
        </div>
        <div class="w-44 py-2">
          <button
            type="button"
            class="text-sm py-1 px-2 rounded-full bg-[#D9D9D9] hover:bg-[#ffe400] cursor-pointer"
            @click="setClassroomId(course.id, 'form')"
          >add</button>
        </div>
      </div>
    </div>
    <div 
      v-else
      class="whitespace-pre-wrap"
    >{{ output }}</div>
  </div>
  <loading v-else/>
</template>

<script>
import Loading from './Loading.vue'
export default {
  components: { Loading },
  props: {
    setClassroomId: Function,
  },
  data() {
    return {
      isLoaded: false,
      output: '',
      courses: [],
    }
  },
  mounted() {
    this.listCourses()
  },
  methods: {
    async listCourses() { // Get Classroom List
      this.isLoaded = false
      this.output = ''
      let response
      try {
        response = await gapi.client.classroom.courses.list()
        const result = response.result
        this.courses = result.courses
      } catch (err) {
        this.output = err.message
        return
      }
      this.isLoaded = true
    },
  },
}
</script>

<style>

</style>

 

StudentList.vue

<template>
  <div 
    v-if="isLoaded"
    class="w-full h-[25rem] flex items-center justify-center"
  >
    <div 
      v-if="students.length"
      class="w-fit h-full"
    >
      <div class="flex items-center border border-[#E5E5E5] rounded-t text-center">
        <div class="w-36 py-2 font-semibold bg-[#E5E5E5]">
          GivenName
        </div>
        <div class="w-36 py-2 font-semibold bg-[#E5E5E5]">
          FullName
        </div>
        <div class="w-16 py-2 font-semibold bg-[#E5E5E5]">
          btn
        </div>
      </div>
      <div
        class="flex items-center border border-[#E5E5E5] text-center last:rounded-b-lg"
        v-for="(student, index) in students"
        :key="`${student.userId}-${index}`"
      >
        <div class="w-36 py-2">
          {{ student.profile.name.givenName }}
        </div>
        <div class="w-36 py-2 border-x">
          {{ student.profile.name.fullName }}
        </div>
        <div class="w-16 py-2">
          <!-- v-if="workInfo?.id" -->
          <button 
            type="button"
            class="text-sm py-1 px-2 rounded-full bg-[#D9D9D9] hover:bg-[#ffe400] cursor-pointer"
            @click="submissionHandler(student)"
          >
            {{ isGrading ? 'reclaim' : 'grading' }}
          </button>
        </div>
      </div>
    </div>
    <div 
      v-else
      class="whitespace-pre-wrap"
    >No students found.</div>
  </div>
  <div 
    v-if="isLoaded"
    class="w-[22rem] h-10 flex items-center justify-end"
  >
    <button
      type="button"
      class="text-sm py-1 px-2 rounded-full bg-[#D9D9D9] hover:bg-[#ffe400] cursor-pointer"
      @click="setClassroomId(null, 'list')"
    >Back</button>
  </div>
  <loading v-else />
</template>

<script>
import Loading from './Loading.vue'
export default {
  components: { Loading },
  props: {
    workInfo: Object,
    classroomId: String,
    setClassroomId: Function,
  },
  data() {
    return {
      isLoaded: false,
      students: [],
      isGrading: false,
    }
  },
  mounted() {
    this.getStudents()
  },
  methods: {
    async getStudents() {
      this.isLoaded = false
      try {
        const response = await gapi.client.classroom.courses.students.list({
          courseId: this.classroomId,
        })
        console.log(response, 'response')
        const students = response.result.students
        if (students && students.length > 0) {
          this.students = students
        } else {
          this.students = []
        }
      } catch (err) {
        console.error('Error retrieving students:', err)
      }
      this.isLoaded = true
    },
    submissionHandler(student) {
      console.log(this.workInfo, 'workInfo!')
      console.log(student, 'studentInfo')
      const params = {
        courseId: parseInt(this.classroomId), // 과정(클래스룸) 아이디
        courseWorkId: parseInt(this.workInfo.id), // 과제 아이디
        id: parseInt(student.userId), // 학생 아이디
      }
      if (!this.isGrading) {
        this.grading(params, this.workInfo.maxPoints)
      } else {
        this.reclaim(params)
      }
    },
    async grading(params, maxPoints) {  // 채점 기능
      // await this.turnIn(params)
      
      const draftGrade = Math.round(Math.random() * maxPoints)
      console.log(draftGrade, 'draftGrade')
      
      await gapi.client.classroom.courses.courseWork.studentSubmissions.patch({
        ...params,
        updateMask: 'draftGrade',  // 업데이트 지정. 여러개는 콤마 구분. null = 전체
        draftGrade, // 임시 점수
        assignedGrade: draftGrade,
        updateMask: 'draftGrade,assignedGrade',
        // resource: {
        //   grade: draftGrade,
        // }
        // resource: {
        //   // assignedGrade: null,  //  최종 점수
        // },
      })
      .then(res => {
        console.log(res.result, 'grading res')
        this.isGrading = true
      })
      .catch(err => {
        console.log(`Error grading:`, err)
      })
    },
    async turnIn(params) {  // 제출(학생)
      const response = await gapi.client.classroom.courses.courseWork.studentSubmissions.turnIn(params)
      console.log(response, 'turnIn response')
    },
    async reclaim(params) {  // 미제출(학생)
      await gapi.client.classroom.courses.courseWork.studentSubmissions.reclaim(params)
        .then(res => {
          console.log(res.result, 'reclaim res')
          this.isGrading = false
        })
        .catch(err => {
          console.log(`Error reclaim:`, err)
        })
    },
  },
}
</script>

<style>

</style>

 

CourseWorkForm.vue

<template>
  <div 
    class="w-full h-[25rem] flex flex-col items-center justify-center gap-y-4"
    v-if="isLoaded"
  >
    <input 
      type="text"
      class="w-full h-12 pl-2 border rounded"
      v-model="resource.title"
      placeholder="title"
    >
    <input 
      type="text"
      class="w-full h-12 pl-2 border rounded"
      v-model="resource.description"
      placeholder="description"
    >
    <input 
      type="number"
      class="w-full h-12 pl-2 border rounded"
      v-model="resource.maxPoints"
      placeholder="maxPoints"
    >
    <input 
      type="text"
      class="w-full h-12 pl-2 border rounded"
      v-model="resource.materials.link.url"
      placeholder="materials.link"
    >
    <div class="w-full h-24">
      <div class="w-full h-12 flex items-center gap-x-4">
        <div class="flex items-center gap-x-2">
          <input 
            type="radio" 
            name="due"
            value="3"
            v-model="dueType"
          >
          <p>3일</p>
        </div>
        <div class="flex items-center gap-x-2">
          <input 
            type="radio" 
            name="due"
            value="5"
            v-model="dueType"
          >
          <p>5일</p>
        </div>
        <div class="flex items-center gap-x-2">
          <input 
            type="radio" 
            name="due"
            value="7"
            v-model="dueType"
          >
          <p>7일</p>
        </div>
        <div class="flex items-center gap-x-2">
          <input 
            type="radio" 
            name="due"
            value="10"
            v-model="dueType"
          >
          <p>10일</p>
        </div>
      </div>
      <div class="w-full h-12 flex items-center gap-x-2">
        <input 
          type="radio" 
          name="due"
          v-model="dueType"
          value="custom"
        >
        <p>지정</p>
        <input 
          type="date"
          class="border rounded p-2 disabled:cursor-not-allowed disabled:bg-[#E5E5E5] disabled:text-[#999]"
          v-model="dueDate"
          :disabled="dueType !== 'custom'"
        >
        <input 
          type="time"
          class="border rounded p-2 disabled:cursor-not-allowed disabled:bg-[#E5E5E5] disabled:text-[#999]"
          v-model="dueTime"
          :disabled="dueType !== 'custom'"
        >
      </div>
    </div>
  </div>
  <div 
    v-if="isLoaded"
    class="w-[22rem] h-10 flex items-center justify-end gap-x-4"
  >
    <button
      type="button"
      class="text-sm py-1 px-2 rounded-full border bg-[#FAFAFA] hover:bg-[#ffe400] cursor-pointer"
      @click="submit"
    >Submit</button>
    <button
      type="button"
      class="text-sm py-1 px-2 rounded-full bg-[#D9D9D9] hover:bg-[#666] cursor-pointer"
      @click="backList"
    >Cancel</button>
  </div>
  <loading v-if="!isLoaded"/>
</template>

<script>
import Loading from './Loading.vue'
export default {
  components: { Loading },
  props: {
    classroomId: String,
    setWorkInfo: Function,
    setClassroomId: Function,
  },
  computed: {
    dueDate: {
      get() {
        const date = this.resource.dueDate
        return `${date.year}-${this.addZero(date.month)}-${this.addZero(date.day)}`
      },
      set(date) {
        date = date.split('-')
        this.resource.dueDate = {
          year: date[0],
          month: date[1],
          day: date[2],
        }
      },
    },
    dueTime: {
      get() {
        const time = this.resource.dueTime
        return `${this.addZero(time.hours)}:${this.addZero(time.minutes)}`
      },
      set(time) {
        time = time.split(':')
        this.resource.dueTime = {
          hours: time[0],
          minutes: time[1],
          seconds: 0,
        }
      },
    },
  },
  data() {
    return {
      isLoaded: true,
      resource: {
        title: '',  // Course Work 제목
        description: '',  // Course Work 설명
        materials: {  // Course Work 첨부할 학습 자료
          // driveFile: {},  // Google Drive 파일 자료
          // youtubeVideo: {}, // YouTube 동영상 자료
          link: {
            url: 'https://19-97.tistory.com'
          },
          // form: {}, // Google Forms 자료
        },
        state: 'published', // draft: 임시, published: 개시, deleted: 삭제
        dueDate: {  // Course Work 마감일
          year: 2023,
          month: 6,
          day: 15,
        },
        dueTime: {  // Course Work 마감 시간
          hours: 0,
          minutes: 0,
          seconds: 0,
        },
        maxPoints: 100,  // 총점
        workType: 'ASSIGNMENT', // COURSE_WORK_TYPE_UNSPECIFIED: 지정된 유형x,
                                // ASSIGNMENT: 과제
                                // SHORT_ANSWER_QUESTION: 단답형 질문
                                // MULTIPLE_CHOICE_QUESTION: 객관식 문제
      },
      dueType: 3,
    }
  },
  mounted() {
    this.setDueDate()
  },
  methods: {
    async submit() {
      this.isLoaded = false
      let params = {
        id: null,
        page: 'list',
      }
      if (this.classroomId) {
        this.setDueDateTimeUTC()
        console.log(this.resource, 'resource!')
        await gapi.client.classroom.courses.courseWork.create({
          courseId: this.classroomId,
          resource: {
            ...this.resource,
          },
        })
          .then(res => {
            console.log(res, 'created courseWork!')
            console.log(this.classroomId, 'classroomId')
            console.log(`course(${res.result.courseId}) make '${res.result.title}'(${res.result.id})!`)
            console.log(`link: ${res.result.alternateLink}`)
            this.setWorkInfo({
              ...this.resource,
              id: res.result.id,
            })
            params = {
              id: this.classroomId,
              page: 'student',
            }
          })
          .catch(err => {
            console.log(`Error courseWork create:`, err)
          })
      }
      this.isLoaded = true
      this.setClassroomId(params.id, params.page)
    },
    backList() {
      this.setClassroomId(null, 'list')
    },
    setDueDate() {  // 셀렉트 박스 선택시 dueDate 설정
      if (isNaN(this.dueType)) return
      let now = new Date()
      now = new Date(now.setDate(now.getDate() + parseInt(this.dueType)))

      this.resource.dueDate = {
        year: now.getFullYear(),
        month: now.getMonth() + 1,
        day: now.getDate(),
      }
      this.resource.dueTime = {
        hours: 0,
        minutes: 0,
        seconds: 0,
      }
    },
    setDueDateTimeUTC() { // 클래스룸에 UTC로 안올리면 KST로 올라가서 9시간 시차 발생하여 변환
      const localDate = new Date(this.resource.dueDate.year, this.resource.dueDate.month, this.resource.dueDate.day, this.resource.dueTime.hours, this.resource.dueTime.minutes)
      console.log(localDate)
      const utcDate = new Date(localDate.getTime() + localDate.getTimezoneOffset() * 60000) // 로컬 시간을 UTC로 변환
      console.log(utcDate)
      this.resource.dueDate = {
        year: utcDate.getFullYear(),
        month: utcDate.getMonth(),
        day: utcDate.getDate(),
      }
      this.resource.dueTime = {
        hours: utcDate.getHours(),
        minutes: utcDate.getMinutes(),
        seconds: 0,
      }
    },
    addZero(num) {
      num = parseInt(num)
      if (num < 10) return `0${num}`
      return num
    },
  },
  watch: {
    dueType() {
      this.setDueDate()
    },
  },
}
</script>

<style>

</style>

 

Loading.vue

<template>
  <div class="w-full h-[25rem] flex items-center justify-center">
    <div class="w-40 h-40 relative">
      <div class="w-full h-full rounded-full overflow-hidden animate-spin">
        <div class="w-full h-1/2 flex">
          <div class="w-1/2 h-full bg-green-300"></div>
          <div class="w-1/2 h-full bg-gray-200"></div>
        </div>
        <div class="w-full h-1/2 flex">
          <div class="w-1/2 h-full bg-gray-200"></div>
          <div class="w-1/2 h-full bg-gray-200"></div>
        </div>
      </div>
      <div class="w-32 h-32 top-4 left-4 rounded-full bg-white absolute flex items-center justify-center">Loading...</div>
    </div>
  </div>
</template>

<script>
export default {
  
}
</script>

<style>

</style>

 

 

반응형