본문으로 바로가기

체점은 아마 제대로 되는 거였던거 같다..

하지만 프론트가 안되서 백엔드에서 작업을 시작 후 완료해버렸으므로

이 부분도 메모합니다.

 

Controller.ts

import { 
  Get, 
  Post,
  Patch,
  Controller, 
  UsePipes, 
  ValidationPipe,
  Query,
  Body,
} from "@nestjs/common"
import { ClassroomService } from "./classroom.service"

@Controller('class/classroom')
@UsePipes(
  new ValidationPipe({
    transform: true,
    transformOptions: {
      enableImplicitConversion: true,
    },
    skipUndefinedProperties: true,
  })
)
export class ClassroomController {
  constructor(private classroomService: ClassroomService) {}

  @Post('/auth')
  auth() {
    return this.classroomService.setAuth()
  }

  @Get()
  listCourses(@Query('courseId') courseId: string) {
    return this.classroomService.listCourses(courseId)
  }

  @Post('/work')
  createCourseWork(@Body() params: any) {
    return this.classroomService.createCourseWork(params)
  }

  @Patch('/work')
  listCourseWorkPatch(@Body() params: any) {
    return this.classroomService.listCourseWorkPatch(params)
  }

  @Get('/worklist')
  worklist(@Query('courseId') courseId: string, @Query('courseWorkId') courseWorkId: string) {
    return this.classroomService.worklist(courseId, courseWorkId)
  }

  @Post('/logout')
  logout() {
    return this.classroomService.logout()
  }

  @Post('/turnin')
  turnIn(@Body() params: any) {
    return this.classroomService.turnIn(params)
  }
}

 

Service.ts

credentials.json은 Google Cloud Console에서 OAuth 클라이언트를 다운받아서

폴더 최상단(tsconfig.json과 같은 위치)에 위치시키면 된다.

import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as process from 'process'
import { authenticate } from '@google-cloud/local-auth'
import { classroom_v1, google } from 'googleapis'

@Injectable()
export class ClassroomService {
  private readonly TOKEN_PATH = path.join(process.cwd(), 'token.json')
  private readonly CREDENTIALS_PATH = path.join(process.cwd(), `credentials${process.env.NODE_ENV === 'production-real' ? '-real' : ''}.json`)
  private readonly SCOPES = [
    'https://www.googleapis.com/auth/classroom.courses.readonly',
    'https://www.googleapis.com/auth/classroom.coursework.students',
    'https://www.googleapis.com/auth/classroom.courseworkmaterials',
    'https://www.googleapis.com/auth/classroom.coursework.me',
    'https://www.googleapis.com/auth/classroom.rosters.readonly',
  ]

  constructor() {}

  private auth: any
  private classroom: classroom_v1.Classroom
  

  private async loadSaveCredentialsIfExist() {
    try {
      const content = await fs.readFile(this.TOKEN_PATH, 'utf-8')
      const credentials = JSON.parse(content)
      return google.auth.fromJSON(credentials)
    } catch (err) {
      console.log('loadSaveCredentialsIfExist ERORR!! =>', err)
      return null
    }
  }

  private async saveCredentials(client) {
    const content = await fs.readFile(this.CREDENTIALS_PATH, 'utf-8')
    const keys = JSON.parse(content)
    const key = keys.installed || keys.web
    const payload = JSON.stringify({
      type: 'authorized_user',
      client_id: key.client_id,
      client_secret: key.client_secret,
      refresh_token: client.credentials.refresh_token,
    })

    await fs.writeFile(this.TOKEN_PATH, payload)
  }

  private async authorize() {
    try {
      let client: any = await this.loadSaveCredentialsIfExist()
      if (client) {
        return client
      }
      client = await authenticate({
        scopes: this.SCOPES,
        keyfilePath: this.CREDENTIALS_PATH,
      })
      if (client.credentials) {
        await this.saveCredentials(client)
      }
      return client
    } catch (error) {
      return error
    }
  }

  /**
   * 인증을 위한 api
   * @returns 
   */
  async setAuth() {
    try {
      if (!this.auth) {
        this.auth = await this.authorize()
      }
      if (!this.classroom) {
        this.classroom = google.classroom({ version: 'v1', auth: this.auth })
      }
    } catch (error) {
      throw error
    }
  }

  /**
   * 클래스룸 수업 목록
   * @param courseId 필수x. 입력시 학생 목록
   * @returns 
   */
  async listCourses(courseId: string) {
    try {
      await this.setAuth()
      let data
      if (!courseId) {
        const res = await this.classroom.courses.list()
        data = res.data.courses
      } else {
        const res = await this.classroom.courses.students.list({ courseId })
        data = res.data.students
      }
  
      return {
        statusCode: HttpStatus.OK,
        message: 'success',
        data,
      }
    } catch (error) {
      return {
        statusCode: error?.code || HttpStatus.BAD_REQUEST,
        message: error?.message || 'error',
        error,
      }
    }
  }

  /**
   * 과제 생성
   * @param params.courseId 
   * @param params.requestBody.state 
   * @param params.requestBody.title
   * @param params.requestBody.description
   * @param params.requestBody.workType
   * @param params.requestBody.meterials
   * @param params.requestBody.dueDate
   * @param params.requestBody.dueTime
   * @param params.requestBody.maxPoints
   */
  async createCourseWork(params: any) {
    try {
      await this.setAuth()
      const res = await this.classroom.courses.courseWork.create({
        ...params,
      })
      const courseWorkId = res.data.id
      const res2 = await this.classroom.courses.courseWork.studentSubmissions.list({
        courseId: params.courseId,
        courseWorkId,
      })
      const courseWorkSubmissions = res2.data.studentSubmissions
      
      return {
        statusCode: HttpStatus.OK,
        message: 'success',
        data: {
          courseId: params.courseId,
          courseWorkId,
          courseWorkSubmissions,
        },
      }
    } catch (error) {
      return {
        statusCode: error?.code || HttpStatus.BAD_REQUEST,
        message: error?.message || 'error',
        error,
      }
    }

  }

  /**
   * 과제 채점
   * @param params.courseId
   * @param params.courseWorkId
   * @param params.studentId
   * @param params.grade
   */
  async listCourseWorkPatch(params: any) {
    try {
      await this.setAuth()
      const res = await this.classroom.courses.courseWork.studentSubmissions.patch({
        courseId: params.courseId,
        courseWorkId: params.courseWorkId,
        id: params.studentId,
        updateMask: 'draftGrade,assignedGrade',
        requestBody: {
          draftGrade: params.grade,
          assignedGrade: params.grade,
        },
      })
      
      return {
        statusCode: HttpStatus.OK,
        message: 'success',
        data: res.data,
      }
    } catch (err) {
      console.log('tlqkf ERROR', err)
      return {
        statusCode: err?.code || HttpStatus.BAD_REQUEST,
        message: 'error',
        errors: err.errors,
        err,
      }
    }
  }

  /**
   * 로그아웃
   * @returns 
   */
  async logout() {
    try {
      await fs.unlink(this.TOKEN_PATH)
      this.auth = null
      this.classroom = null
      return {
        statusCode: HttpStatus.OK,
        message: '로그아웃 되었습니다.',
      }
    } catch (err) {
      return {
        statusCode: err?.code || HttpStatus.BAD_REQUEST,
        message: '로그아웃 실패',
        error: err,
      }
    }
  }

  /**
   * 과제 제출 (학생)
   * @param params.courseId 
   * @param params.courseWorkId 
   * @param params.studentId 
   */
  async turnIn(params: any) {
    try {
      await this.setAuth()

      const response = await this.classroom.courses.courseWork.studentSubmissions.turnIn({
        courseId: params.courseId,
        courseWorkId: params.courseWorkId,
        id: params.studentId,
      })

      console.log('Assignment turned in:', response.data)

      return {
        statusCode: HttpStatus.OK,
        message: 'success',
        data: response.data,
      }
    } catch (error) {
      console.error('Error turning in assignment:', error);
      return {
        statusCode: error?.code || HttpStatus.BAD_REQUEST,
        message: 'error',
        error,
      }
    }
  }

  /**
   * 과제/제출 목록
   * @param courseId  // 클래스룸 아이디
   * @param courseWorkId  // 과제 아이디. 없을 경우 과제 목록, 있을 경우 제출 목록
   */
  async worklist(courseId: string, courseWorkId: string) {
    if (!courseId) {
      throw new HttpException('Valid courseId needed.', HttpStatus.BAD_REQUEST)
    }

    try {
      await this.setAuth()

      let res
      if (courseWorkId) {
        res = await this.classroom.courses.courseWork.studentSubmissions.list({
          courseId,
          courseWorkId,
        })
      } else {
        res = await this.classroom.courses.courseWork.list({
          courseId,
        })
      }

      return {
        statusCode: HttpStatus.OK,
        message: 'success',
        data: res.data,
      }
    } catch (err) {
      console.log('qudtls ERROR', err)
      return {
        statusCode: err?.code || HttpStatus.BAD_REQUEST,
        message: 'error',
        errors: err.errors,
        err,
      }
    }
  }
}

 

참고로 studentSubmissions.patch에서 studentId는 사용자ID가 아니라 과제별 개별 아이디가 존재했다..

왜 학생 아이디라고 설명해놔서 나를 헤매게 만들었는가..

(제출 목록에서 id가 넣어야할 값이다.. userId(학생ID)가 아니라)

반응형