From f9f0ffadd994384254b86eb3798641aca291eda1 Mon Sep 17 00:00:00 2001 From: Choi-Jung-Hyeon Date: Sat, 23 May 2026 01:54:35 +0000 Subject: [PATCH 1/6] fix(be): allow querying past contest submissions from public problem page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 대회 종료 후 공개 문제로 전환된 problem에서 유저가 대회 당시 제출했던 submission을 조회할 때 EntityNotExistException이 발생하는 버그를 수정한다. Root cause: - getSubmission의 submission 조회 where 조건에 contestId: null이 그대로 전달되어 Prisma가 WHERE contest_id IS NULL을 생성, contestId가 채워진 과거 대회 submission을 찾지 못함 Fix: - contestId/assignmentId가 null인 경우 undefined로 변환하여 해당 필드를 where 조건에서 제외 (no filter) - submission 조회 후 실제 contestId가 존재하면 해당 대회 정보를 로드하여 진행 중인 대회의 타인 제출 열람 차단 로직을 정상 동작하도록 보완 Security: - contestId 없이 공개 엔드포인트를 호출하더라도 submission의 실제 contestId가 진행 중인 대회이면 ForbiddenAccessException으로 차단 Co-Authored-By: Claude Sonnet 4.6 --- .../src/submission/submission.service.ts | 36 ++++++++- .../test/submission.service.spec.ts | 81 +++++++++++++++++++ 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/apps/backend/apps/client/src/submission/submission.service.ts b/apps/backend/apps/client/src/submission/submission.service.ts index 2fe6c2b206..0daaf1b38d 100644 --- a/apps/backend/apps/client/src/submission/submission.service.ts +++ b/apps/backend/apps/client/src/submission/submission.service.ts @@ -1125,11 +1125,15 @@ export class SubmissionService { where: { id, problemId, - contestId, - assignmentId + // contestId/assignmentId가 null인 경우 해당 필드를 필터에서 제외한다. + // null로 그대로 넘기면 Prisma가 WHERE contest_id IS NULL 조건을 생성하여 + // 대회 종료 후 공개된 문제에서 과거 대회 제출 기록을 조회할 수 없게 되는 버그가 발생한다. + contestId: contestId ?? undefined, + assignmentId: assignmentId ?? undefined }, select: { userId: true, + contestId: true, user: { select: { username: true @@ -1153,6 +1157,34 @@ export class SubmissionService { throw new EntityNotExistException('Submission') } + // contestId가 요청에 포함되지 않았지만 실제 submission에는 contestId가 있는 경우 + // (대회 종료 후 공개된 문제에서 과거 대회 제출 기록을 조회하는 케이스) + // 해당 submission이 속한 대회 정보를 로드하여 진행 중인 대회의 타인 제출 열람을 차단한다. + if (!contestId && submission.contestId) { + const contestRecord = await this.prisma.contestRecord.findUnique({ + where: { + // eslint-disable-next-line @typescript-eslint/naming-convention + contestId_userId: { + contestId: submission.contestId, + userId + } + }, + select: { + contest: { + select: { + startTime: true, + endTime: true, + isJudgeResultVisible: true + } + } + } + }) + if (contestRecord) { + contest = contestRecord.contest + isJudgeResultVisible = contestRecord.contest.isJudgeResultVisible + } + } + // 본인이나 관리자가 아닐 경우 if ( submission.userId !== userId && diff --git a/apps/backend/apps/client/src/submission/test/submission.service.spec.ts b/apps/backend/apps/client/src/submission/test/submission.service.spec.ts index 14ad02e6bb..6f1f2b78e2 100644 --- a/apps/backend/apps/client/src/submission/test/submission.service.spec.ts +++ b/apps/backend/apps/client/src/submission/test/submission.service.spec.ts @@ -603,6 +603,87 @@ describe('SubmissionService', () => { }) ).to.be.rejectedWith(ForbiddenAccessException) }) + + it('should return own past-contest submission when accessed from public problem page (contestId=null)', async () => { + // 대회 종료 후 공개된 문제에서 본인이 대회 당시 제출한 submission을 조회하는 케이스 + // submission의 contestId가 CONTEST_ID이지만 요청에서 contestId=null로 호출됨 + const endedContest = { + ...mockContest, + startTime: new Date(Date.now() - 20000), + endTime: new Date(Date.now() - 10000), // 이미 종료된 대회 + isJudgeResultVisible: true + } + const testcaseResult = submissionResults.map((result) => { + return { + ...result, + cpuTime: + result.cpuTime || result.cpuTime === BigInt(0) + ? result.cpuTime.toString() + : null + } + }) + + db.problem.findFirst.resolves(problems[0]) + db.submission.findFirst.resolves({ + ...submissions[0], + contestId: CONTEST_ID, // 대회 당시 제출이므로 contestId가 채워져 있음 + user: { username: 'username' }, + submissionResult: submissionResults + }) + db.contestRecord.findUnique.resolves({ + contest: endedContest + }) + + expect( + await service.getSubmission({ + id: submissions[0].id, + problemId: problems[0].id, + userId: submissions[0].userId, // 본인 조회 + userRole: Role.User, + contestId: null, // 공개 문제 페이지에서 contestId 없이 호출 + assignmentId: null + }) + ).to.deep.equal({ + problemId: problems[0].id, + username: 'username', + code: submissions[0].code.map((snippet) => snippet.text).join('\n'), + language: submissions[0].language, + createTime: submissions[0].createTime, + result: submissions[0].result, + testcaseResult + }) + }) + + it('should block viewing other users past-contest submission via public page when contest is still ongoing', async () => { + // contestId=null로 공개 엔드포인트를 호출하더라도 + // submission의 실제 contestId가 진행 중인 대회이면 타인 제출 열람을 차단해야 함 + const ongoingContest = { + ...mockContest, + startTime: new Date(Date.now() - 10000), + endTime: new Date(Date.now() + 10000) + } + + db.problem.findFirst.resolves(problems[0]) + db.submission.findFirst.resolves({ + ...submissions[0], + contestId: CONTEST_ID, // 진행 중인 대회의 submission + userId: 2 // 타인의 submission + }) + db.contestRecord.findUnique.resolves({ + contest: ongoingContest + }) + + await expect( + service.getSubmission({ + id: submissions[0].id, + problemId: problems[0].id, + userId: 1, // 다른 유저가 조회 시도 + userRole: Role.User, + contestId: null, // contestId 없이 공개 엔드포인트 호출 + assignmentId: null + }) + ).to.be.rejectedWith(ForbiddenAccessException) + }) }) describe('getContestSubmissions', () => { From d0e238b5344a33828b4170b03e476bef88e8186c Mon Sep 17 00:00:00 2001 From: Choi-Jung-Hyeon Date: Sat, 23 May 2026 02:33:07 +0000 Subject: [PATCH 2/6] refactor(be): enforce submission privacy using contest relatio --- .devcontainer/devcontainer-lock.json | 24 ++++++++ .../src/submission/submission.service.ts | 55 +++++++++---------- .../test/submission.service.spec.ts | 48 ++++++++++++---- 3 files changed, 88 insertions(+), 39 deletions(-) create mode 100644 .devcontainer/devcontainer-lock.json diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000000..2edb305a48 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,24 @@ +{ + "features": { + "ghcr.io/devcontainers-extra/features/pnpm:2": { + "version": "2.0.5", + "resolved": "ghcr.io/devcontainers-extra/features/pnpm@sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6", + "integrity": "sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6" + }, + "ghcr.io/devcontainers/features/go:1": { + "version": "1.3.4", + "resolved": "ghcr.io/devcontainers/features/go@sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032", + "integrity": "sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032" + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "1.8.0", + "resolved": "ghcr.io/devcontainers/features/java@sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a", + "integrity": "sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a" + }, + "ghcr.io/devcontainers/features/terraform:1": { + "version": "1.4.3", + "resolved": "ghcr.io/devcontainers/features/terraform@sha256:503d0888b3a43a32335e08cd4477f8c4629404ad83f6a36f0e0fee7f567c06c7", + "integrity": "sha256:503d0888b3a43a32335e08cd4477f8c4629404ad83f6a36f0e0fee7f567c06c7" + } + } +} diff --git a/apps/backend/apps/client/src/submission/submission.service.ts b/apps/backend/apps/client/src/submission/submission.service.ts index 0daaf1b38d..5c05c56ebb 100644 --- a/apps/backend/apps/client/src/submission/submission.service.ts +++ b/apps/backend/apps/client/src/submission/submission.service.ts @@ -1125,15 +1125,29 @@ export class SubmissionService { where: { id, problemId, - // contestId/assignmentId가 null인 경우 해당 필드를 필터에서 제외한다. + // contestId가 null인 경우 해당 필드를 필터에서 제외한다. // null로 그대로 넘기면 Prisma가 WHERE contest_id IS NULL 조건을 생성하여 // 대회 종료 후 공개된 문제에서 과거 대회 제출 기록을 조회할 수 없게 되는 버그가 발생한다. + // assignmentId는 null 여부 자체가 보안상 의미가 있으므로 필터를 유지한다. contestId: contestId ?? undefined, - assignmentId: assignmentId ?? undefined + assignmentId }, select: { userId: true, - contestId: true, + contest: { + select: { + startTime: true, + endTime: true, + isJudgeResultVisible: true + } + }, + assignment: { + select: { + startTime: true, + endTime: true, + isJudgeResultVisible: true + } + }, user: { select: { username: true @@ -1157,32 +1171,17 @@ export class SubmissionService { throw new EntityNotExistException('Submission') } - // contestId가 요청에 포함되지 않았지만 실제 submission에는 contestId가 있는 경우 + // contestId/assignmentId가 요청에 포함되지 않았지만 실제 submission에 contest/assignment가 있는 경우 // (대회 종료 후 공개된 문제에서 과거 대회 제출 기록을 조회하는 케이스) - // 해당 submission이 속한 대회 정보를 로드하여 진행 중인 대회의 타인 제출 열람을 차단한다. - if (!contestId && submission.contestId) { - const contestRecord = await this.prisma.contestRecord.findUnique({ - where: { - // eslint-disable-next-line @typescript-eslint/naming-convention - contestId_userId: { - contestId: submission.contestId, - userId - } - }, - select: { - contest: { - select: { - startTime: true, - endTime: true, - isJudgeResultVisible: true - } - } - } - }) - if (contestRecord) { - contest = contestRecord.contest - isJudgeResultVisible = contestRecord.contest.isJudgeResultVisible - } + // submission의 릴레이션 데이터를 직접 사용하여 진행 중인 대회의 타인 제출 열람을 차단한다. + // contestRecord를 별도로 조회하지 않으므로, 대회 비참가자도 보안 검사를 우회할 수 없다. + if (!contest && submission.contest) { + contest = submission.contest + isJudgeResultVisible = submission.contest.isJudgeResultVisible + } + if (!assignment && submission.assignment) { + assignment = submission.assignment + isHiddenTestcaseVisible = submission.assignment.isJudgeResultVisible } // 본인이나 관리자가 아닐 경우 diff --git a/apps/backend/apps/client/src/submission/test/submission.service.spec.ts b/apps/backend/apps/client/src/submission/test/submission.service.spec.ts index 6f1f2b78e2..dc0bd46443 100644 --- a/apps/backend/apps/client/src/submission/test/submission.service.spec.ts +++ b/apps/backend/apps/client/src/submission/test/submission.service.spec.ts @@ -606,7 +606,7 @@ describe('SubmissionService', () => { it('should return own past-contest submission when accessed from public problem page (contestId=null)', async () => { // 대회 종료 후 공개된 문제에서 본인이 대회 당시 제출한 submission을 조회하는 케이스 - // submission의 contestId가 CONTEST_ID이지만 요청에서 contestId=null로 호출됨 + // submission의 contest 릴레이션을 통해 대회 정보를 직접 가져옴 const endedContest = { ...mockContest, startTime: new Date(Date.now() - 20000), @@ -626,13 +626,10 @@ describe('SubmissionService', () => { db.problem.findFirst.resolves(problems[0]) db.submission.findFirst.resolves({ ...submissions[0], - contestId: CONTEST_ID, // 대회 당시 제출이므로 contestId가 채워져 있음 + contest: endedContest, // 대회 당시 제출이므로 contest 릴레이션이 존재 user: { username: 'username' }, submissionResult: submissionResults }) - db.contestRecord.findUnique.resolves({ - contest: endedContest - }) expect( await service.getSubmission({ @@ -654,9 +651,10 @@ describe('SubmissionService', () => { }) }) - it('should block viewing other users past-contest submission via public page when contest is still ongoing', async () => { + it('should block viewing other users submission from ongoing contest via public page', async () => { // contestId=null로 공개 엔드포인트를 호출하더라도 - // submission의 실제 contestId가 진행 중인 대회이면 타인 제출 열람을 차단해야 함 + // submission의 contest가 진행 중인 대회이면 타인 제출 열람을 차단해야 함 + // submission의 릴레이션 데이터를 직접 사용하므로 contestRecord 유무와 무관하게 차단됨 const ongoingContest = { ...mockContest, startTime: new Date(Date.now() - 10000), @@ -666,12 +664,9 @@ describe('SubmissionService', () => { db.problem.findFirst.resolves(problems[0]) db.submission.findFirst.resolves({ ...submissions[0], - contestId: CONTEST_ID, // 진행 중인 대회의 submission + contest: ongoingContest, // 진행 중인 대회의 릴레이션 데이터 userId: 2 // 타인의 submission }) - db.contestRecord.findUnique.resolves({ - contest: ongoingContest - }) await expect( service.getSubmission({ @@ -684,6 +679,37 @@ describe('SubmissionService', () => { }) ).to.be.rejectedWith(ForbiddenAccessException) }) + + it('should block non-participant from viewing submission of ongoing contest via public page', async () => { + // 대회에 참가하지 않은 유저가 공개 엔드포인트를 통해 + // 진행 중인 대회의 타인 제출물을 조회 시도하는 케이스 + // contestRecord가 없어도 submission.contest 릴레이션으로 차단되어야 함 + const ongoingContest = { + ...mockContest, + startTime: new Date(Date.now() - 10000), + endTime: new Date(Date.now() + 10000) + } + + db.problem.findFirst.resolves(problems[0]) + db.submission.findFirst.resolves({ + ...submissions[0], + contest: ongoingContest, // 진행 중인 대회의 릴레이션 데이터 + userId: 2 // 타인의 submission + }) + // 비참가자이므로 contestRecord가 없음 + db.contestRecord.findUnique.resolves(null) + + await expect( + service.getSubmission({ + id: submissions[0].id, + problemId: problems[0].id, + userId: 1, // 대회에 참가하지 않은 유저 + userRole: Role.User, + contestId: null, + assignmentId: null + }) + ).to.be.rejectedWith(ForbiddenAccessException) + }) }) describe('getContestSubmissions', () => { From 42f4b5b4f039f21ee6d45335a07a5392be32b310 Mon Sep 17 00:00:00 2001 From: Choi-Jung-Hyeon Date: Sat, 23 May 2026 02:33:07 +0000 Subject: [PATCH 3/6] fix(be): use submission relation to prevent security bypass in getSubmission - Revert assignmentId filter to keep WHERE assignment_id IS NULL (prevents assignment submissions from leaking to public pages) - Select contest/assignment relations directly in submission query instead of separate contestRecord lookup - Non-participants can no longer bypass ongoing contest access check by omitting contestId parameter - Add non-participant security test case --- .../src/submission/submission.service.ts | 55 +++++++++---------- .../test/submission.service.spec.ts | 48 ++++++++++++---- 2 files changed, 64 insertions(+), 39 deletions(-) diff --git a/apps/backend/apps/client/src/submission/submission.service.ts b/apps/backend/apps/client/src/submission/submission.service.ts index 0daaf1b38d..5c05c56ebb 100644 --- a/apps/backend/apps/client/src/submission/submission.service.ts +++ b/apps/backend/apps/client/src/submission/submission.service.ts @@ -1125,15 +1125,29 @@ export class SubmissionService { where: { id, problemId, - // contestId/assignmentId가 null인 경우 해당 필드를 필터에서 제외한다. + // contestId가 null인 경우 해당 필드를 필터에서 제외한다. // null로 그대로 넘기면 Prisma가 WHERE contest_id IS NULL 조건을 생성하여 // 대회 종료 후 공개된 문제에서 과거 대회 제출 기록을 조회할 수 없게 되는 버그가 발생한다. + // assignmentId는 null 여부 자체가 보안상 의미가 있으므로 필터를 유지한다. contestId: contestId ?? undefined, - assignmentId: assignmentId ?? undefined + assignmentId }, select: { userId: true, - contestId: true, + contest: { + select: { + startTime: true, + endTime: true, + isJudgeResultVisible: true + } + }, + assignment: { + select: { + startTime: true, + endTime: true, + isJudgeResultVisible: true + } + }, user: { select: { username: true @@ -1157,32 +1171,17 @@ export class SubmissionService { throw new EntityNotExistException('Submission') } - // contestId가 요청에 포함되지 않았지만 실제 submission에는 contestId가 있는 경우 + // contestId/assignmentId가 요청에 포함되지 않았지만 실제 submission에 contest/assignment가 있는 경우 // (대회 종료 후 공개된 문제에서 과거 대회 제출 기록을 조회하는 케이스) - // 해당 submission이 속한 대회 정보를 로드하여 진행 중인 대회의 타인 제출 열람을 차단한다. - if (!contestId && submission.contestId) { - const contestRecord = await this.prisma.contestRecord.findUnique({ - where: { - // eslint-disable-next-line @typescript-eslint/naming-convention - contestId_userId: { - contestId: submission.contestId, - userId - } - }, - select: { - contest: { - select: { - startTime: true, - endTime: true, - isJudgeResultVisible: true - } - } - } - }) - if (contestRecord) { - contest = contestRecord.contest - isJudgeResultVisible = contestRecord.contest.isJudgeResultVisible - } + // submission의 릴레이션 데이터를 직접 사용하여 진행 중인 대회의 타인 제출 열람을 차단한다. + // contestRecord를 별도로 조회하지 않으므로, 대회 비참가자도 보안 검사를 우회할 수 없다. + if (!contest && submission.contest) { + contest = submission.contest + isJudgeResultVisible = submission.contest.isJudgeResultVisible + } + if (!assignment && submission.assignment) { + assignment = submission.assignment + isHiddenTestcaseVisible = submission.assignment.isJudgeResultVisible } // 본인이나 관리자가 아닐 경우 diff --git a/apps/backend/apps/client/src/submission/test/submission.service.spec.ts b/apps/backend/apps/client/src/submission/test/submission.service.spec.ts index 6f1f2b78e2..dc0bd46443 100644 --- a/apps/backend/apps/client/src/submission/test/submission.service.spec.ts +++ b/apps/backend/apps/client/src/submission/test/submission.service.spec.ts @@ -606,7 +606,7 @@ describe('SubmissionService', () => { it('should return own past-contest submission when accessed from public problem page (contestId=null)', async () => { // 대회 종료 후 공개된 문제에서 본인이 대회 당시 제출한 submission을 조회하는 케이스 - // submission의 contestId가 CONTEST_ID이지만 요청에서 contestId=null로 호출됨 + // submission의 contest 릴레이션을 통해 대회 정보를 직접 가져옴 const endedContest = { ...mockContest, startTime: new Date(Date.now() - 20000), @@ -626,13 +626,10 @@ describe('SubmissionService', () => { db.problem.findFirst.resolves(problems[0]) db.submission.findFirst.resolves({ ...submissions[0], - contestId: CONTEST_ID, // 대회 당시 제출이므로 contestId가 채워져 있음 + contest: endedContest, // 대회 당시 제출이므로 contest 릴레이션이 존재 user: { username: 'username' }, submissionResult: submissionResults }) - db.contestRecord.findUnique.resolves({ - contest: endedContest - }) expect( await service.getSubmission({ @@ -654,9 +651,10 @@ describe('SubmissionService', () => { }) }) - it('should block viewing other users past-contest submission via public page when contest is still ongoing', async () => { + it('should block viewing other users submission from ongoing contest via public page', async () => { // contestId=null로 공개 엔드포인트를 호출하더라도 - // submission의 실제 contestId가 진행 중인 대회이면 타인 제출 열람을 차단해야 함 + // submission의 contest가 진행 중인 대회이면 타인 제출 열람을 차단해야 함 + // submission의 릴레이션 데이터를 직접 사용하므로 contestRecord 유무와 무관하게 차단됨 const ongoingContest = { ...mockContest, startTime: new Date(Date.now() - 10000), @@ -666,12 +664,9 @@ describe('SubmissionService', () => { db.problem.findFirst.resolves(problems[0]) db.submission.findFirst.resolves({ ...submissions[0], - contestId: CONTEST_ID, // 진행 중인 대회의 submission + contest: ongoingContest, // 진행 중인 대회의 릴레이션 데이터 userId: 2 // 타인의 submission }) - db.contestRecord.findUnique.resolves({ - contest: ongoingContest - }) await expect( service.getSubmission({ @@ -684,6 +679,37 @@ describe('SubmissionService', () => { }) ).to.be.rejectedWith(ForbiddenAccessException) }) + + it('should block non-participant from viewing submission of ongoing contest via public page', async () => { + // 대회에 참가하지 않은 유저가 공개 엔드포인트를 통해 + // 진행 중인 대회의 타인 제출물을 조회 시도하는 케이스 + // contestRecord가 없어도 submission.contest 릴레이션으로 차단되어야 함 + const ongoingContest = { + ...mockContest, + startTime: new Date(Date.now() - 10000), + endTime: new Date(Date.now() + 10000) + } + + db.problem.findFirst.resolves(problems[0]) + db.submission.findFirst.resolves({ + ...submissions[0], + contest: ongoingContest, // 진행 중인 대회의 릴레이션 데이터 + userId: 2 // 타인의 submission + }) + // 비참가자이므로 contestRecord가 없음 + db.contestRecord.findUnique.resolves(null) + + await expect( + service.getSubmission({ + id: submissions[0].id, + problemId: problems[0].id, + userId: 1, // 대회에 참가하지 않은 유저 + userRole: Role.User, + contestId: null, + assignmentId: null + }) + ).to.be.rejectedWith(ForbiddenAccessException) + }) }) describe('getContestSubmissions', () => { From 291ff6afc8549ed82f66a92ea85e9916e9255b00 Mon Sep 17 00:00:00 2001 From: Choi-Jung-Hyeon Date: Sat, 23 May 2026 02:40:31 +0000 Subject: [PATCH 4/6] chore(be): add devcontainer-lock.json to pin feature versions --- .devcontainer/devcontainer-lock.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .devcontainer/devcontainer-lock.json diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000000..2edb305a48 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,24 @@ +{ + "features": { + "ghcr.io/devcontainers-extra/features/pnpm:2": { + "version": "2.0.5", + "resolved": "ghcr.io/devcontainers-extra/features/pnpm@sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6", + "integrity": "sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6" + }, + "ghcr.io/devcontainers/features/go:1": { + "version": "1.3.4", + "resolved": "ghcr.io/devcontainers/features/go@sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032", + "integrity": "sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032" + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "1.8.0", + "resolved": "ghcr.io/devcontainers/features/java@sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a", + "integrity": "sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a" + }, + "ghcr.io/devcontainers/features/terraform:1": { + "version": "1.4.3", + "resolved": "ghcr.io/devcontainers/features/terraform@sha256:503d0888b3a43a32335e08cd4477f8c4629404ad83f6a36f0e0fee7f567c06c7", + "integrity": "sha256:503d0888b3a43a32335e08cd4477f8c4629404ad83f6a36f0e0fee7f567c06c7" + } + } +} From a3dac178f6431eb3b7002ebc11e441dcdc8daddf Mon Sep 17 00:00:00 2001 From: fourmi103 <101015120+Choi-Jung-Hyeon@users.noreply.github.com> Date: Mon, 25 May 2026 22:19:14 +0900 Subject: [PATCH 5/6] chore(be): devcontainer/devcontainer-lock.json --- .devcontainer/devcontainer-lock.json | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 .devcontainer/devcontainer-lock.json diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json deleted file mode 100644 index 2edb305a48..0000000000 --- a/.devcontainer/devcontainer-lock.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "features": { - "ghcr.io/devcontainers-extra/features/pnpm:2": { - "version": "2.0.5", - "resolved": "ghcr.io/devcontainers-extra/features/pnpm@sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6", - "integrity": "sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6" - }, - "ghcr.io/devcontainers/features/go:1": { - "version": "1.3.4", - "resolved": "ghcr.io/devcontainers/features/go@sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032", - "integrity": "sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032" - }, - "ghcr.io/devcontainers/features/java:1": { - "version": "1.8.0", - "resolved": "ghcr.io/devcontainers/features/java@sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a", - "integrity": "sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a" - }, - "ghcr.io/devcontainers/features/terraform:1": { - "version": "1.4.3", - "resolved": "ghcr.io/devcontainers/features/terraform@sha256:503d0888b3a43a32335e08cd4477f8c4629404ad83f6a36f0e0fee7f567c06c7", - "integrity": "sha256:503d0888b3a43a32335e08cd4477f8c4629404ad83f6a36f0e0fee7f567c06c7" - } - } -} From e0d69cf1cc8ac367b27bae945d08d85614958cde Mon Sep 17 00:00:00 2001 From: Choi-Jung-Hyeon Date: Mon, 25 May 2026 13:19:50 +0000 Subject: [PATCH 6/6] fix(fe): remove unnecessary assignment check in submission logic --- apps/backend/apps/client/src/submission/submission.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/backend/apps/client/src/submission/submission.service.ts b/apps/backend/apps/client/src/submission/submission.service.ts index 5c05c56ebb..024dd8a2f7 100644 --- a/apps/backend/apps/client/src/submission/submission.service.ts +++ b/apps/backend/apps/client/src/submission/submission.service.ts @@ -1179,10 +1179,6 @@ export class SubmissionService { contest = submission.contest isJudgeResultVisible = submission.contest.isJudgeResultVisible } - if (!assignment && submission.assignment) { - assignment = submission.assignment - isHiddenTestcaseVisible = submission.assignment.isJudgeResultVisible - } // 본인이나 관리자가 아닐 경우 if (