아무리 검색해도 관련 코드가 없었다..
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>
반응형
'JavaScript > Vue.js' 카테고리의 다른 글
Nuxt에서 localhost를 https로 열기 (0) | 2022.08.30 |
---|---|
Vue에서 XLSX(SheetJS) 사용한 것 정리 (0) | 2022.03.30 |
Vue 컴포넌트에 외부 스크립트 추가 (메모용) (0) | 2022.03.18 |
[vue-gtag] Google Analytics 공부 (0) | 2022.01.11 |
Vue.js 페이징처리(Pagination) (0) | 2021.09.08 |