diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index f9922cfa..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "" -labels: "" -assignees: "" ---- - -## 어떤 버그인가요? - -> 어떤 버그인지 간결하게 설명해주세요. - -

- -## 어떤 상황에서 발생한 버그인가요? - -> (가능하면) Given-When-Then 형식으로 서술해주세요. - -

- -## 예상 결과 - -> 예상했던 정상적인 결과가 어떤 것이었는지 설명해주세요. - -

- -## 참고자료 - -> 관련 문서, 스크린샷, 또는 예시 등이 있다면 여기에 첨부해주세요. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..a009b742 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,44 @@ +name: Bug report +description: 버그를 보고하여 서비스를 개선하는 데 도움을 주세요. +title: "[bug]: " +body: + - type: markdown + attributes: + value: | + ## 버그 보고서 + 문제를 정확히 파악하기 위해 아래 정보를 최대한 자세히 작성해 주세요. + - type: textarea + id: bug-description + attributes: + label: 어떤 버그인가요? + description: 발생한 버그를 간략하고 명확하게 설명해 주세요. + placeholder: 예시) 로그인 버튼을 눌렀을 때 반응이 없습니다. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: 재현 경로 + description: 버그가 발생하는 상황을 단계별로 설명해 주세요 (가능하면 Given-When-Then 형식). + placeholder: | + 1. 메인 페이지에 접속합니다. + 2. 로그인 버튼을 클릭합니다. + 3. ... + validations: + required: true + - type: textarea + id: expected-results + attributes: + label: 예상 결과 + description: 원래 기대했던 동작은 무엇이었나요? + placeholder: 로그인 팝업이 뜨고 아이디/비밀번호 입력창이 보여야 합니다. + validations: + required: true + - type: textarea + id: references + attributes: + label: 참고 자료 (선택) + description: 스크린샷, 오류 로그, 또는 관련 문서 링크를 첨부해 주세요. + placeholder: 이미지나 로그 파일을 드래그 앤 드롭 하세요. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/etc.md b/.github/ISSUE_TEMPLATE/etc.md deleted file mode 100644 index 2075d9ed..00000000 --- a/.github/ISSUE_TEMPLATE/etc.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: ETC -about: refactoring or minor updates -title: "" -labels: "" -assignees: "" ---- - -## 개발 유형 - -> 해당되는 항목을 선택해주세요. - -- [ ] 기존 기능 개선 -- [ ] 성능 개선 -- [ ] (필요시 추가 가능) - -

- -## 작업이 필요한 이유 - -> 왜 이 작업이 필요한지 설명해주세요. 작업의 목적과 기대 효과를 기술해주세요. - -

- -## 작업 상세 내용 - -- [ ] TODO -- [ ] TODO -- [ ] TODO - -

- -## 참고 자료 - -> 관련 문서, 스크린샷, 또는 예시 등이 있다면 여기에 첨부해주세요. diff --git a/.github/ISSUE_TEMPLATE/etc.yml b/.github/ISSUE_TEMPLATE/etc.yml new file mode 100644 index 00000000..8cc43968 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/etc.yml @@ -0,0 +1,45 @@ +name: ETC +description: 리팩토링, 성능 개선 등 기타 작업을 위한 템플릿입니다. +title: "[etc]: " +body: + - type: markdown + attributes: + value: | + ## 기타 작업 + 기능 추가나 버그 수정 외의 작업(리팩토링, 환경 설정 등) 내용을 작성해 주세요. + - type: checkboxes + id: development-type + attributes: + label: 작업 유형 + description: 해당되는 항목을 모두 선택해 주세요. + options: + - label: 리팩토링 (Refactoring) + - label: 성능 개선 (Performance) + - label: 환경 설정 (Config) + - label: 문서 수정 (Docs) + - label: 기타 (Etc) + - type: textarea + id: rationale + attributes: + label: 작업 배경 + description: 왜 이 작업이 필요한지 설명해 주세요. + placeholder: 코드의 가독성을 높이기 위해 불필요한 코드를 제거합니다. + validations: + required: true + - type: textarea + id: task-details + attributes: + label: 작업 상세 내용 + description: 어떤 작업을 진행할 것인지 자세히 적어주세요. + placeholder: | + - [ ] 컴포넌트 A 분리 + - [ ] 공통 훅 추출 + validations: + required: true + - type: textarea + id: references + attributes: + label: 참고 자료 (선택) + description: 관련 링크, 스크린샷 등이 있다면 첨부해 주세요. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 704e83ed..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[feat] " -labels: "" -assignees: "" ---- - -## 어떤 기능인가요? - -> 추가하려는 기능 또는 개선하려는 부분에 대해 간결하게 설명해주세요. - -

- -## 작업 상세 - -- [ ] TODO -- [ ] TODO -- [ ] TODO - -

- -## 참고자료 - -> 관련 문서, 스크린샷, 또는 예시 등이 있다면 여기에 첨부해주세요. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..0b8a520a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,34 @@ +name: Feature request +description: 새로운 기능이나 아이디어를 제안해 주세요. +title: "[feat]: " +body: + - type: markdown + attributes: + value: | + ## 기능 제안 + 새로운 기능을 제안하거나 기존 기능을 개선하고 싶으신가요? 아래 내용을 작성해 주세요. + - type: textarea + id: feature-description + attributes: + label: 어떤 기능인가요? + description: 제안하려는 기능의 목적과 기대 효과를 간략히 설명해 주세요. + placeholder: 예시) 다크 모드 기능을 추가하여 시력을 보호하고 싶습니다. + validations: + required: true + - type: textarea + id: task-details + attributes: + label: 작업 상세 내용 + description: 어떻게 구현하면 좋을지, 어떤 작업들이 필요한지 적어주세요. + placeholder: | + - [ ] 테마 상태 관리 추가 + - [ ] CSS 변수 적용 + validations: + required: true + - type: textarea + id: references + attributes: + label: 참고 자료 (선택) + description: 디자인 시안, 관련 라이브러리, 또는 참고할 만한 예시 링크를 첨부해 주세요. + validations: + required: false diff --git a/src/assets/svg/retrieveIcon.svg b/src/assets/svg/retrieveIcon.svg new file mode 100644 index 00000000..9503465b --- /dev/null +++ b/src/assets/svg/retrieveIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/entities/assignment/api/assignmentApi.ts b/src/entities/assignment/api/assignmentApi.ts index cd07d7b2..0e358a0b 100644 --- a/src/entities/assignment/api/assignmentApi.ts +++ b/src/entities/assignment/api/assignmentApi.ts @@ -5,6 +5,7 @@ import { assignmentCodeSchema, assignmentDetailSchema, assignmentScheduleSchema, + assignmentSubmissionHistorySchema, assignmentSubmissionResultSchema, type TAssignmentForm, } from '@/entities/assignment/model/schemas'; @@ -110,3 +111,17 @@ export const submitAssignment = async ( ); return parsed.response; }; + +// 과제 제출 이력 조회 API +export const getAssignmentSubmissionHistory = async ( + unitId: number, + assignmentId: number +) => { + const response = await privateAxios.get( + ENDPOINTS.ASSIGNMENTS.SUBMIT(unitId, assignmentId) + ); + const parsed = apiResponseSchema(assignmentSubmissionHistorySchema).parse( + response.data + ); + return parsed.response; +}; diff --git a/src/entities/assignment/api/assignmentQueries.ts b/src/entities/assignment/api/assignmentQueries.ts index 6389b77a..432d5130 100644 --- a/src/entities/assignment/api/assignmentQueries.ts +++ b/src/entities/assignment/api/assignmentQueries.ts @@ -5,6 +5,7 @@ import { getAssignment, getAssignmentsByCourse, getAssignmentSchedules, + getAssignmentSubmissionHistory, } from '@/entities/assignment/api/assignmentApi'; export const assignmentQueries = { @@ -48,4 +49,12 @@ export const assignmentQueries = { queryKey: ['code', codeId], queryFn: () => getAssignmentCode(codeId), }), + + // 과제 제출 이력 조회 쿼리 옵션 + getAssignmentSubmissionHistory: (unitId: number, assignmentId: number) => + queryOptions({ + queryKey: ['assignments', unitId, assignmentId, 'submissionHistory'], + queryFn: () => getAssignmentSubmissionHistory(unitId, assignmentId), + select: (data) => [...data.submissionList].reverse(), + }), }; diff --git a/src/entities/assignment/model/schemas.ts b/src/entities/assignment/model/schemas.ts index a38c2337..3c4e7f9f 100644 --- a/src/entities/assignment/model/schemas.ts +++ b/src/entities/assignment/model/schemas.ts @@ -68,6 +68,17 @@ export const assignmentCodeSchema = z.object({ language: z.string(), }); +export const assignmentSubmissionHistorySchema = z.object({ + assignmentId: z.number(), + submissionList: z.array( + z.object({ + codeId: z.number(), + isSuccess: z.boolean(), + submittedAt: z.string(), + }) + ), +}); + export type TAssignment = z.infer; export type TAssignmentSchedule = z.infer; export type TAssignmentForm = z.infer; @@ -75,3 +86,6 @@ export type TAssignmentDetail = z.infer; export type TAssignmentSubmissionResult = z.infer< typeof assignmentSubmissionResultSchema >; +export type TAssignmentSubmissionHistory = z.infer< + typeof assignmentSubmissionHistorySchema +>; diff --git a/src/features/assignment/submit-assignment/lib/useAssignmentSubmission.ts b/src/features/assignment/submit-assignment/lib/useAssignmentSubmission.ts index 1c861db2..21715fb1 100644 --- a/src/features/assignment/submit-assignment/lib/useAssignmentSubmission.ts +++ b/src/features/assignment/submit-assignment/lib/useAssignmentSubmission.ts @@ -1,4 +1,5 @@ import {assignmentMutations} from '@/entities/assignment/api/assignmentMutations'; +import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; import type {TAssignmentSubmissionResult} from '@/entities/assignment/model/schemas'; import {courseQueries} from '@/entities/course/api/courseQueries'; import type {TCourseOverview} from '@/entities/course/model/schemas'; @@ -6,19 +7,16 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'; import {useState} from 'react'; export const useAssignmentSubmission = ( + unitId: number, courseDetails: TCourseOverview, - assignmentId: number + assignmentId: number, + onSubmitSuccess?: (codeId: number) => void ) => { const [result, setResult] = useState( null ); const [isModalOpen, setIsModalOpen] = useState(false); - // unitId 찾기 - const unitId = courseDetails.units.find((unit) => - unit.assignments.some((assignment) => assignment.id === assignmentId) - )?.id; - const queryClient = useQueryClient(); // 과제 제출 API 호출 @@ -28,6 +26,10 @@ export const useAssignmentSubmission = ( queryClient.invalidateQueries({ queryKey: courseQueries.getCourseDetails(courseDetails.id).queryKey, }); + queryClient.invalidateQueries({ + queryKey: assignmentQueries.getAssignmentSubmissionHistory(unitId, assignmentId).queryKey, + }); + onSubmitSuccess?.(data.codeId); setIsModalOpen(true); setResult(data); }, diff --git a/src/pages/dashboard/ui/ScheduleCard.tsx b/src/pages/dashboard/ui/ScheduleCard.tsx index e996a35e..82fc5913 100644 --- a/src/pages/dashboard/ui/ScheduleCard.tsx +++ b/src/pages/dashboard/ui/ScheduleCard.tsx @@ -20,7 +20,7 @@ const ScheduleCard = ({ - {remainingDays} + {remainingDays == 0 ? '오늘' : `${remainingDays}일 전`} diff --git a/src/pages/submit-assignment/AssignmentSubmitPage.tsx b/src/pages/submit-assignment/AssignmentSubmitPage.tsx index 665095c5..14b6d894 100644 --- a/src/pages/submit-assignment/AssignmentSubmitPage.tsx +++ b/src/pages/submit-assignment/AssignmentSubmitPage.tsx @@ -15,17 +15,23 @@ import ChatQuestionModal from '@/features/chat/ui/ChatQuestionModal'; import ChatIcon from '@/assets/svg/chatIcon.svg?react'; import {useState, useRef} from 'react'; import type {CodeEditorRef} from './ui/CodeEditor'; +import SubmissionHistoryPanel from './ui/SubmissionHistoryPanel'; const AssignmentSubmitPage = () => { const [isChatOpen, setIsChatOpen] = useState(false); + const [isTerminalOpen, setIsTerminalOpen] = useState(false); const editorRef = useRef(null); const location = useLocation(); const {courseId, assignmentId} = useParams(); - const {index, codeId} = (location.state ?? {}) as { + const {index, codeId: initialCodeId} = (location.state ?? {}) as { index?: number; codeId?: number; }; + const [currentCodeId, setCurrentCodeId] = useState( + initialCodeId + ); + const [{data: assignment}, {data: courseDetails}] = useSuspenseQueries({ queries: [ assignmentQueries.getAssignment(Number(assignmentId)), @@ -33,17 +39,50 @@ const AssignmentSubmitPage = () => { ], }); + // unitId 찾기 + const unitId = courseDetails.units.find((unit) => + unit.assignments.some( + (assignment) => assignment.id === Number(assignmentId) + ) + )?.id; + + const {data: submissionList} = useQuery({ + ...assignmentQueries.getAssignmentSubmissionHistory( + Number(unitId), + Number(assignmentId) + ), + enabled: !!unitId, + }); + const {data: assignmentCode} = useQuery({ - ...assignmentQueries.getAssignmentCode(Number(codeId || 0)), - enabled: !!codeId, + ...assignmentQueries.getAssignmentCode(Number(currentCodeId || 0)), + enabled: !!currentCodeId, }); const {runCode, output, isRunning} = useCodeExecution(Number(assignmentId)); const {onSubmit, result, isSubmitPending, isModalOpen, closeModal} = - useAssignmentSubmission(courseDetails, Number(assignmentId)); + useAssignmentSubmission( + Number(unitId), + courseDetails, + Number(assignmentId), + setCurrentCodeId + ); + + const isEditorReady = !currentCodeId || (currentCodeId && assignmentCode); - const isEditorReady = !codeId || (codeId && assignmentCode); + const onTerminalToggle = () => { + setIsTerminalOpen(!isTerminalOpen); + }; + + const handleRunCode = (code: string, input: string) => { + setIsTerminalOpen(true); + runCode(code, input); + }; + + const onRetrieve = (codeId: number) => { + setCurrentCodeId(codeId); + }; return ( <> @@ -68,12 +107,14 @@ const AssignmentSubmitPage = () => { ) : ( <> @@ -84,8 +125,19 @@ const AssignmentSubmitPage = () => { - - + + {isTerminalOpen ? ( + + ) : ( + + )} diff --git a/src/pages/submit-assignment/ui/CodeEditor.tsx b/src/pages/submit-assignment/ui/CodeEditor.tsx index 23f8e151..88fb748b 100644 --- a/src/pages/submit-assignment/ui/CodeEditor.tsx +++ b/src/pages/submit-assignment/ui/CodeEditor.tsx @@ -46,6 +46,8 @@ interface CodeEditorProps { isRunning: boolean; assignmentCode?: string; id: string; + onTerminalToggle: () => void; + isTerminalOpen: boolean; } export interface CodeEditorRef { @@ -54,7 +56,16 @@ export interface CodeEditorRef { const CodeEditor = forwardRef( ( - {onSubmit, isSubmitPending, runCode, isRunning, assignmentCode, id}, + { + onSubmit, + isSubmitPending, + runCode, + isRunning, + assignmentCode, + id, + onTerminalToggle, + isTerminalOpen, + }, ref ) => { const editorRef = useRef(null); @@ -88,7 +99,14 @@ const CodeEditor = forwardRef( return (
-
+
+
+ + + ))} + + )} + + ); +}; + +export default SubmissionHistoryPanel; diff --git a/src/pages/submit-assignment/ui/Terminal.tsx b/src/pages/submit-assignment/ui/Terminal.tsx index ed06860b..4349fa1b 100644 --- a/src/pages/submit-assignment/ui/Terminal.tsx +++ b/src/pages/submit-assignment/ui/Terminal.tsx @@ -3,7 +3,7 @@ interface TerminalProps { } const Terminal = ({output}: TerminalProps) => { return ( -
+
{output && (
{output} diff --git a/src/shared/config/endpoints.ts b/src/shared/config/endpoints.ts index ceecc565..47955aca 100644 --- a/src/shared/config/endpoints.ts +++ b/src/shared/config/endpoints.ts @@ -32,7 +32,8 @@ export const ENDPOINTS = { DETAIL: (chatRoomId: number | string) => `/chats/${chatRoomId}`, }, ENROLLMENTS: { - BY_COURSE: (courseId: number | string) => `/courses/${courseId}/enrollments`, + BY_COURSE: (courseId: number | string) => + `/courses/${courseId}/enrollments`, DETAIL: (courseId: number | string, memberId: number | string) => `/courses/${courseId}/enrollments/${memberId}`, }, diff --git a/src/shared/lib/course.ts b/src/shared/lib/course.ts index 2c9800b1..95d32802 100644 --- a/src/shared/lib/course.ts +++ b/src/shared/lib/course.ts @@ -1,5 +1,6 @@ import type {TUnit} from '@/entities/unit/model/schemas'; import type {SemesterCode} from '@/shared/model/types'; +import {ensureUTC} from '@/shared/lib/chat'; const SEMESTER_MAP: Record = { FIRST: '1', @@ -76,3 +77,12 @@ export const formatCourseOptionLabel = ( ) => { return `${title} ${formatCourseTermWithSlash(year, semester, section)}`; }; + +// 제출 날짜로부터 며칠 전인지 포맷팅 +export const formatDaysAgo = (isoString: string): string => { + const midnight = (d: Date) => d.setHours(0, 0, 0, 0); + const days = Math.round( + (midnight(new Date()) - midnight(new Date(ensureUTC(isoString)))) / 86400000 + ); + return days === 0 ? '오늘' : `${days}일 전`; +}; diff --git a/src/shared/ui/badge/Badge.tsx b/src/shared/ui/badge/Badge.tsx index d2deb6e7..28cf9b9d 100644 --- a/src/shared/ui/badge/Badge.tsx +++ b/src/shared/ui/badge/Badge.tsx @@ -49,7 +49,7 @@ const Badge = (props: BadgeProps) => { case 'schedule': return ( - {props.children}일 전 + {props.children} );