From e1719861e77b398d1a42ebfa3c9e966296dd4048 Mon Sep 17 00:00:00 2001 From: kimwuju Date: Wed, 14 Oct 2026 18:05:26 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EC=9D=98=20UTC=20=EB=8B=B5=EB=B3=80=20=EC=8B=9C=EA=B0=81?= =?UTF-8?q?=EC=9D=84=20=EC=9D=B4=20=EC=84=9C=EB=B2=84=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EC=98=AE=EA=B2=A8=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 채팅 서버는 UTC 컨테이너에서 시간대 없이 LocalDateTime.now()를 answerCreatedAt으로 보낸다. 그대로 저장해, 이 서버가 UTC가 아닐 때(로컬 KST) 답변만 이 서버가 찍은 질문 시각보다 9시간 이르게 남았다(면접 819~822). 받은 값을 UTC로 읽어 ZoneId.systemDefault()로 옮긴다. 둘 다 UTC인 운영에서는 값이 그대로다. 채팅 서버가 UTC라고 가정한다. 다른 시간대에서 돌리면 다시 어긋나므로, 채팅 서버가 오프셋을 실어 보내는 것이 근본 해결이다. 테스트는 기본 시간대를 KST로 바꿔 돌려, UTC인 CI에서도 변환이 빠지면 실패한다. Co-Authored-By: Claude Opus 5.5 --- .../dto/request/SaveInterviewRequest.java | 1 + .../interview/service/InterviewService.java | 17 ++++++++++++++++- .../InterviewServiceSaveResultTest.java | 18 ++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/request/SaveInterviewRequest.java b/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/request/SaveInterviewRequest.java index 3510dddc..2faadb50 100644 --- a/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/request/SaveInterviewRequest.java +++ b/src/main/java/repit/repit_api_server/domain/userdata/interview/dto/request/SaveInterviewRequest.java @@ -75,6 +75,7 @@ public static class Answer { private Long questionId; private Integer responseTime; private String answerContent; + // 채팅 서버의 시각이라 UTC다. 시간대 없이 오므로 저장할 때 이 서버 시간대로 옮긴다. private LocalDateTime answerCreatedAt; } } diff --git a/src/main/java/repit/repit_api_server/domain/userdata/interview/service/InterviewService.java b/src/main/java/repit/repit_api_server/domain/userdata/interview/service/InterviewService.java index 337d5544..2ed9344d 100644 --- a/src/main/java/repit/repit_api_server/domain/userdata/interview/service/InterviewService.java +++ b/src/main/java/repit/repit_api_server/domain/userdata/interview/service/InterviewService.java @@ -37,6 +37,8 @@ import repit.repit_api_server.global.exception.BusinessException; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; @@ -432,6 +434,19 @@ private LocalDateTime orNow(LocalDateTime createdAt) { return createdAt == null ? LocalDateTime.now() : createdAt; } + /** + * 채팅 서버가 찍은 시각을 이 서버의 시각으로 옮긴다. + * + *

채팅 서버는 UTC 컨테이너에서 시간대 없이 LocalDateTime.now()를 보낸다. 그대로 저장하면 이 서버가 UTC가 + * 아닐 때(로컬 KST) 답변만 이 서버가 찍은 질문 시각보다 9시간 이르게 남는다. + */ + // ponytail: 채팅 서버가 UTC라고 가정한다. 다른 시간대에서 돌리면 어긋나므로, 채팅 서버가 오프셋을 실어 보내면 그 값을 쓴다. + private LocalDateTime fromChatClock(LocalDateTime chatTime) { + return chatTime == null ? null : chatTime.atOffset(ZoneOffset.UTC) + .atZoneSameInstant(ZoneId.systemDefault()) + .toLocalDateTime(); + } + /** * 질문 의도. 채점은 이 값 하나를 기준으로 이뤄진다. * @@ -474,7 +489,7 @@ private void saveAnswers(InterviewEntity interview, List savedAnswers() { assertThat(answers).extracting(AnswerEntity::getQuestionId).containsExactly(901L, 902L); assertThat(answers).extracting(AnswerEntity::getUserId).containsOnly(7L); assertThat(answers.get(0).getContent()).isEqualTo("스레드가 I/O 대기에 묶였습니다."); - assertThat(answers.get(0).getCreatedAt()) - .isEqualTo(LocalDateTime.parse("2026-08-18T01:01:30")); + } + + @Test + void 채팅_서버의_UTC_답변_시각을_이_서버_시간대로_옮긴다() { + // CI는 UTC라 그대로 두면 옮기지 않아도 통과한다. 로컬처럼 KST에서 돌려 본다. + TimeZone original = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + try { + service.saveInterview(request()); + + assertThat(savedAnswers().get(0).getCreatedAt()) + .isEqualTo(LocalDateTime.parse("2026-08-18T10:01:30")); + } finally { + TimeZone.setDefault(original); + } } @Test