Skip to content
2 changes: 1 addition & 1 deletion deploy-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ docker run -d \
--name memory-app \
--restart unless-stopped \
-p 8081:8081 \
-e SPRING_DATASOURCE_URL="jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true" \
-e SPRING_DATASOURCE_URL="jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true" \
-e SPRING_DATASOURCE_USERNAME="${DB_USERNAME}" \
-e SPRING_DATASOURCE_PASSWORD="${DB_PASSWORD}" \
-e CLOUD_AWS_CREDENTIALS_ACCESS_KEY="${AWS_ACCESS_KEY_ID}" \
Expand Down
2 changes: 1 addition & 1 deletion deploy-production-direct.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ docker run -d \
--network "$NETWORK_NAME" \
--restart unless-stopped \
-p 8081:8081 \
-e SPRING_DATASOURCE_URL="jdbc:mysql://memory-mysql:3306/$MYSQL_DATABASE?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true" \
-e SPRING_DATASOURCE_URL="jdbc:mysql://memory-mysql:3306/$MYSQL_DATABASE?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true" \
-e SPRING_DATASOURCE_USERNAME="$MYSQL_USER" \
-e SPRING_DATASOURCE_PASSWORD="$MYSQL_PASSWORD" \
"$IMAGE_NAME:$IMAGE_TAG"
Expand Down
2 changes: 1 addition & 1 deletion deploy-production-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ deploy_docker() {
--name memory-app \
--restart unless-stopped \
-p 8081:8081 \
-e SPRING_DATASOURCE_URL="jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true" \
-e SPRING_DATASOURCE_URL="jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true" \
-e SPRING_DATASOURCE_USERNAME="${DB_USERNAME}" \
-e SPRING_DATASOURCE_PASSWORD="${DB_PASSWORD}" \
-e CLOUD_AWS_CREDENTIALS_ACCESS_KEY="${AWS_ACCESS_KEY_ID}" \
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ services:
mysql:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/memory_db?useSSL=false&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/memory_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: memory_user
SPRING_DATASOURCE_PASSWORD: memory_password
ports:
Expand Down
13 changes: 8 additions & 5 deletions src/main/java/zim/tave/memory/domain/Board.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -23,9 +24,9 @@ public class Board {
private String title;

@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
private OffsetDateTime createdAt;
Copy link
Contributor

@chwwwon chwwwon Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OffsetDateTime을 μ‚¬μš©ν•˜λŠ” 도메인에 κ³΅ν†΅μ μœΌλ‘œ μ μš©λ˜λŠ” λ¦¬λ·°μž…λ‹ˆλ‹€.
MySQL의 DATETIME / TIMESTAMPλŠ” offset 정보λ₯Ό μ €μž₯ν•˜μ§€ μ•Šμ•„μ„œ 버전에 따라

  • offset 손싀
  • μžλ™ λ³€ν™˜ 였λ₯˜
  • λŸ°νƒ€μž„ λ§€ν•‘ μ—λŸ¬ κ°€λŠ₯
    λ“±μ˜ λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆλ‹€κ³  ν•©λ‹ˆλ‹€.
    μ½”νŒŒμΌλŸΏμ΄ μ•„λž˜μ™€ 같이 μ œμ•ˆν•΄μ£Όμ—ˆλŠ”λ° OffsetDateTimeAttributeConverter 파일 μΆ”κ°€λ₯Ό κ³ λ €ν•΄λ³΄λŠ” 것도 쒋을 것 κ°™μ•„μš”!

ꢌμž₯ μˆ˜μ • (κ°€μž₯ μ•ˆμ „ν•œ 방법)
πŸ‘‰ JPA AttributeConverter μΆ”κ°€ (DB λ³€κ²½ 없이 ν•΄κ²°)
μƒˆ 파일 μΆ”κ°€
src/main/java/zim/tave/memory/config/OffsetDateTimeAttributeConverter.java

@Converter(autoApply = true)
public class OffsetDateTimeAttributeConverter
        implements AttributeConverter<OffsetDateTime, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(OffsetDateTime odt) {
        return odt == null ? null : Timestamp.from(odt.toInstant());
    }

    @Override
    public OffsetDateTime convertToEntityAttribute(Timestamp ts) {
        return ts == null ? null
            : OffsetDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC);
    }
}


private LocalDateTime updatedAt;
private OffsetDateTime updatedAt;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "userId", nullable = false)
Expand All @@ -40,11 +41,13 @@ public class Board {

@PrePersist
public void prePersist() {
createdAt = LocalDateTime.now();
if (this.createdAt == null) {
createdAt = OffsetDateTime.now(ZoneOffset.UTC);
}
}

@PreUpdate
public void preUpdate() {
updatedAt = LocalDateTime.now();
updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
}
}
15 changes: 9 additions & 6 deletions src/main/java/zim/tave/memory/domain/Diary.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import lombok.Setter;
import org.hibernate.annotations.BatchSize;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -33,7 +34,7 @@ public class Diary {
private String city;

@Column(nullable = false)
private LocalDateTime dateTime;
private OffsetDateTime dateTime;

@Lob
@Column(nullable = false, length = 88)
Expand All @@ -54,14 +55,16 @@ public class Diary {
private List<DiaryImage> diaryImages = new ArrayList<>();

@Column(updatable = false)
private LocalDateTime createdAt;
private OffsetDateTime createdAt;

@Column(nullable = false)
private Boolean isStored;

@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
if (this.createdAt == null) {
this.createdAt = OffsetDateTime.now(ZoneOffset.UTC);
}
}

public void addDiaryImage(DiaryImage image) {
Expand All @@ -70,15 +73,15 @@ public void addDiaryImage(DiaryImage image) {
}

public static Diary createDiary(User user, Trip trip, Country country, String city,
LocalDateTime dateTime, String content) {
OffsetDateTime dateTime, String content) {
Diary diary = new Diary();
diary.setUser(user);
diary.setTrip(trip);
diary.setCountry(country);
diary.setCity(city);
diary.setDateTime(dateTime);
diary.setContent(content != null ? content : ""); // contentλŠ” 선택 ν•„λ“œμ΄λ―€λ‘œ null인 경우 빈 λ¬Έμžμ—΄λ‘œ 처리
diary.setCreatedAt(dateTime != null ? dateTime : LocalDateTime.now());
diary.setCreatedAt(dateTime != null ? dateTime : OffsetDateTime.now(ZoneOffset.UTC));
diary.setIsStored(false);
return diary;
}
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/zim/tave/memory/domain/Trip.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.Setter;

import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -55,7 +56,7 @@ public class Trip {
@PrePersist
protected void onCreate() {
if (this.startDate == null) {
this.startDate = LocalDate.now();
this.startDate = LocalDate.now(ZoneOffset.UTC);
}
if (this.endDate == null) {
this.endDate = this.startDate;
Expand All @@ -76,8 +77,8 @@ public static Trip createTrip(User user, String tripName, String description, Tr
trip.setTripName(tripName);
trip.setDescription(description);
trip.setTripTheme(tripTheme);
trip.setStartDate(LocalDate.now());
trip.setEndDate(LocalDate.now());
trip.setStartDate(LocalDate.now(ZoneOffset.UTC));
trip.setEndDate(LocalDate.now(ZoneOffset.UTC));
return trip;
}

Expand Down
9 changes: 6 additions & 3 deletions src/main/java/zim/tave/memory/domain/VisitedCountry.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

@Entity
@Getter
Expand All @@ -19,7 +20,7 @@ public class VisitedCountry {

private String color;

private LocalDateTime createdAt;
private OffsetDateTime createdAt;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "userId")
Expand All @@ -35,6 +36,8 @@ public class VisitedCountry {

@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
if (this.createdAt == null) {
this.createdAt = OffsetDateTime.now(ZoneOffset.UTC);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"tripId": 1,
"countryCode": "KR",
"city": "μ œμ£Όμ‹œ",
"dateTime": "2025-11-30T11:20:01.570Z",
"content": "μ˜€λŠ˜μ€ μ œμ£Όλ„μ—μ„œ λ©‹μ§„ ν•˜λ£¨λ₯Ό λ³΄λƒˆλ‹€.",
"images": [
{
Expand Down Expand Up @@ -56,14 +55,6 @@ public class CreateDiaryRequest {
@Schema(description = "λ„μ‹œλͺ… (ν•„μˆ˜)", example = "μ œμ£Όμ‹œ")
private String city;

@Schema(
description = """
일기 μž‘μ„± λ‚ μ§œ 및 μ‹œκ°„ (ISO 8601 ν˜•μ‹) : μžλ™ μƒμ„±λ©λ‹ˆλ‹€.
""",
example = "2025-11-30T11:20:01.570Z"
)
private String dateTime;

@Schema(description = "일기 λ‚΄μš©", example = "μ˜€λŠ˜μ€ μ œμ£Όλ„μ—μ„œ λ©‹μ§„ ν•˜λ£¨λ₯Ό λ³΄λƒˆλ‹€.")
private String content;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package zim.tave.memory.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import zim.tave.memory.domain.Board;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.OffsetDateTime;

@Getter
@AllArgsConstructor
Expand All @@ -24,21 +24,21 @@ public class BoardCreateResponseDto {
@Schema(description = "λ³΄λ“œ ν…Œλ§ˆ ID", example = "3")
private Long boardThemeId;

@Schema(description = "λ³΄λ“œ 생성 μ‹œκ°", example = "2025-12-01T14:20:00")
private String createdAt;
@Schema(description = "λ³΄λ“œ 생성 μ‹œκ° (UTC κΈ°μ€€, ISO-8601 ν˜•μ‹). ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλŠ” μ‚¬μš©μžμ˜ 둜컬 νƒ€μž„μ‘΄μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ ν‘œμ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€.", example = "2025-12-01T14:20:00.000Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ν˜„μž¬ 각 DTO ν•„λ“œλ§ˆλ‹€ @jsonformat이 κ°œλ³„ μ§€μ •λ˜μ–΄ μžˆλŠ”λ°, 이 경우 "Z" / "+00:00" 혼재 κ°€λŠ₯성이 μžˆμ–΄ μ „μ—­ Jackson μ„€μ •μœΌλ‘œ UTC + ISO-8601 κ³ μ •ν•˜λŠ” 방법은 μ–΄λ–¨κΉŒμš”?
application.yml에 μ•„λž˜μ™€ 같이 μΆ”κ°€ν•˜λŠ” 방법도 있고, configλ₯Ό μΆ”κ°€ν•˜λŠ” 방법도 μžˆμœΌλ‹ˆ 고렀해보면 쒋을 것 κ°™μ•„μš©

spring:
  jackson:
    time-zone: UTC
    serialization:
      write-dates-as-timestamps: false

private OffsetDateTime createdAt;

@Schema(description = "λ³΄λ“œ μ΅œμ’… μˆ˜μ • μ‹œκ°", example = "2025-12-01T14:20:00")
private String updatedAt;
@Schema(description = "λ³΄λ“œ μ΅œμ’… μˆ˜μ • μ‹œκ° (UTC κΈ°μ€€, ISO-8601 ν˜•μ‹). ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλŠ” μ‚¬μš©μžμ˜ 둜컬 νƒ€μž„μ‘΄μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ ν‘œμ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€.", example = "2025-12-01T14:20:00.000Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private OffsetDateTime updatedAt;

public static BoardCreateResponseDto from(Board board) {
DateTimeFormatter fmt = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

return new BoardCreateResponseDto(
board.getBoardId(),
board.getTitle(),
board.getBoardTheme().getBoardThemeId(),
board.getCreatedAt().format(fmt),
board.getUpdatedAt().format(fmt)
board.getCreatedAt(),
board.getUpdatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package zim.tave.memory.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -8,8 +9,7 @@
import zim.tave.memory.domain.Board;
import zim.tave.memory.domain.BoardStickerMap;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.OffsetDateTime;
import java.util.List;

@Getter
Expand All @@ -31,25 +31,25 @@ public class BoardDetailResponseDto {
@Schema(description = "ν…Œλ§ˆ 썸넀일 URL")
private String thumbnailUrl;

@Schema(description = "생성 μ‹œκ°")
private String createdAt;
@Schema(description = "생성 μ‹œκ° (UTC κΈ°μ€€, ISO-8601 ν˜•μ‹). ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλŠ” μ‚¬μš©μžμ˜ 둜컬 νƒ€μž„μ‘΄μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ ν‘œμ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€.", example = "2025-12-01T14:20:00.000Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private OffsetDateTime createdAt;

@Schema(description = "μˆ˜μ • μ‹œκ°")
private String updatedAt;
@Schema(description = "μˆ˜μ • μ‹œκ° (UTC κΈ°μ€€, ISO-8601 ν˜•μ‹). ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλŠ” μ‚¬μš©μžμ˜ 둜컬 νƒ€μž„μ‘΄μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ ν‘œμ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€.", example = "2025-12-01T17:30:00.000Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private OffsetDateTime updatedAt;

@Schema(description = "μŠ€ν‹°μ»€ λͺ©λ‘")
private List<BoardStickerDetailDto> stickers;

public static BoardDetailResponseDto from(Board board, List<BoardStickerMap> stickerMaps) {
DateTimeFormatter fmt = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

return BoardDetailResponseDto.builder()
.boardId(board.getBoardId())
.title(board.getTitle())
.boardThemeId(board.getBoardTheme().getBoardThemeId())
.thumbnailUrl(board.getBoardTheme().getThumbnailUrl())
.createdAt(board.getCreatedAt().format(fmt))
.updatedAt(board.getUpdatedAt().format(fmt))
.createdAt(board.getCreatedAt())
.updatedAt(board.getUpdatedAt())
.stickers(stickerMaps.stream()
.map(BoardStickerDetailDto::from)
.toList())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package zim.tave.memory.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import zim.tave.memory.domain.Board;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.OffsetDateTime;
import java.util.List;

@Getter
Expand All @@ -23,11 +23,13 @@ public class BoardInformResponseDto {
@Schema(description = "λ³΄λ“œ ν…Œλ§ˆ ID", example = "3")
private Long boardThemeId;

@Schema(description = "λ³΄λ“œ 생성 μ‹œκ°", example = "2025-12-01T14:20:00")
private String createdAt;
@Schema(description = "λ³΄λ“œ 생성 μ‹œκ° (UTC κΈ°μ€€, ISO-8601 ν˜•μ‹). ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλŠ” μ‚¬μš©μžμ˜ 둜컬 νƒ€μž„μ‘΄μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ ν‘œμ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€.", example = "2025-12-01T14:20:00.000Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private OffsetDateTime createdAt;

@Schema(description = "λ³΄λ“œ μ΅œμ’… μˆ˜μ • μ‹œκ°", example = "2025-12-01T14:20:00")
private String updatedAt;
@Schema(description = "λ³΄λ“œ μ΅œμ’… μˆ˜μ • μ‹œκ° (UTC κΈ°μ€€, ISO-8601 ν˜•μ‹). ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλŠ” μ‚¬μš©μžμ˜ 둜컬 νƒ€μž„μ‘΄μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ ν‘œμ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€.", example = "2025-12-01T14:20:00.000Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private OffsetDateTime updatedAt;

@Schema(description = "λ³΄λ“œμ— ν¬ν•¨λœ μŠ€ν‹°μ»€ 개수", example = "3")
private int stickerCount;
Expand All @@ -36,14 +38,12 @@ public class BoardInformResponseDto {
private List<String> stickerUrls;

public static BoardInformResponseDto from(Board board, List<String> stickerUrls) {
DateTimeFormatter fmt = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

return new BoardInformResponseDto(
board.getBoardId(),
board.getTitle(),
board.getBoardTheme().getBoardThemeId(),
board.getCreatedAt().format(fmt),
board.getUpdatedAt().format(fmt),
board.getCreatedAt(),
board.getUpdatedAt(),
stickerUrls.size(),
stickerUrls
);
Expand Down
Loading