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
72 changes: 72 additions & 0 deletions src/main/java/zim/tave/memory/config/JacksonConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package zim.tave.memory.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;

@Configuration
public class JacksonConfig {

private static final DateTimeFormatter OFFSET_DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");

@Bean
public Jackson2ObjectMapperBuilderCustomizer utcOffsetDateTimeCustomizer() {
return builder -> {
// Ensure UTC baseline
builder.timeZone(TimeZone.getTimeZone("UTC"));
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

// Force OffsetDateTime format globally (UTC -> "Z")
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(OffsetDateTime.class, new JsonSerializer<>() {
@Override
public void serialize(OffsetDateTime value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
if (value == null) {
gen.writeNull();
return;
}
gen.writeString(value.format(OFFSET_DATE_TIME_FORMATTER));
}
});
javaTimeModule.addDeserializer(OffsetDateTime.class, new JsonDeserializer<>() {
@Override
public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String raw = p.getValueAsString();
if (raw == null || raw.isBlank()) {
return null;
}
return OffsetDateTime.parse(raw, OFFSET_DATE_TIME_FORMATTER);
}
});

builder.modules(javaTimeModule);
};
}

/**
* Helper for normalizing an OffsetDateTime to UTC when needed.
* (Not wired automatically; kept for consistency utilities if required.)
*/
public static OffsetDateTime normalizeToUtc(OffsetDateTime value) {
return value == null ? null : value.withOffsetSameInstant(ZoneOffset.UTC);
}
}

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
Expand Up @@ -6,8 +6,7 @@
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 +23,19 @@ 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")
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")
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
Expand Up @@ -8,8 +8,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 +30,23 @@ 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")
private OffsetDateTime createdAt;

@Schema(description = "μˆ˜μ • μ‹œκ°")
private String updatedAt;
@Schema(description = "μˆ˜μ • μ‹œκ° (UTC κΈ°μ€€, ISO-8601 ν˜•μ‹). ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλŠ” μ‚¬μš©μžμ˜ 둜컬 νƒ€μž„μ‘΄μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ ν‘œμ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€.", example = "2025-12-01T17:30:00.000Z")
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
Loading
Loading