Skip to content
2 changes: 0 additions & 2 deletions src/main/java/life/mosu/mosuserver/MosuServerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@ConfigurationPropertiesScan
@SpringBootApplication
public class MosuServerApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public List<FaqResponse.AttachmentResponse> toAttachmentResponses(FaqJpaEntity f
s3Service.getPreSignedUrl(
attachment.getS3Key(),
Duration.ofMinutes(s3Properties.getPresignedUrlExpirationMinutes())
)
),
attachment.getS3Key()
))
.toList();
}
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/life/mosu/mosuserver/application/faq/FaqService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.presentation.faq.dto.FaqCreateRequest;
import life.mosu.mosuserver.presentation.faq.dto.FaqResponse;
import life.mosu.mosuserver.presentation.faq.dto.FaqUpdateRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
Expand Down Expand Up @@ -41,6 +42,26 @@ public List<FaqResponse> getFaqWithAttachments(int page, int size) {
.toList();
}

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public FaqResponse getFaqDetail(Long faqId) {
FaqJpaEntity faq = faqRepository.findById(faqId)
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.FAQ_NOT_FOUND));

return toFaqResponse(faq);
}

@Transactional
public void update(FaqUpdateRequest request, Long faqId) {
FaqJpaEntity faqEntity = faqRepository.findById(faqId)
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.FAQ_NOT_FOUND));

faqEntity.update(request.question(), request.answer(), request.author());
faqRepository.save(faqEntity);

Choose a reason for hiding this comment

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

medium

Within a @Transactional method, an explicit call to faqRepository.save(faqEntity) is redundant. JPA's dirty checking mechanism will automatically detect changes to the faqEntity and persist them to the database when the transaction commits. Removing this line will make the code cleaner.


attachmentService.deleteAttachment(faqEntity);
attachmentService.createAttachment(request.attachments(), faqEntity);
}


@Transactional
public void deleteFaq(Long faqId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public void deleteNotice(Long noticeId) {
public void updateNotice(Long noticeId, NoticeUpdateRequest request) {
NoticeJpaEntity noticeEntity = getNoticeOrThrow(noticeId);

noticeEntity.update(request.title(), request.content());
noticeEntity.update(request.title(), request.content(), request.author());
attachmentService.deleteAttachment(noticeEntity);
attachmentService.createAttachment(request.attachments(), noticeEntity);
}
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/life/mosu/mosuserver/domain/faq/FaqJpaEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,24 @@ public class FaqJpaEntity extends BaseTimeEntity {
@Column(name = "answer", nullable = false)
private String answer;

@Column(name = "author", nullable = false)
private String author;

@Column(name = "user_id", nullable = false)
private Long userId;

@Builder
public FaqJpaEntity(final String question, final String answer, final Long userId) {
public FaqJpaEntity(final String question, final String answer, final Long userId,
final String author) {
this.question = question;
this.answer = answer;
this.userId = userId;
this.author = author;
}

public void update(final String question, final String answer, final String author) {
this.question = question;
this.answer = answer;
this.author = author;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,29 @@ public class NoticeJpaEntity extends BaseTimeEntity {
@Column(name = "user_id", nullable = false)
private Long userId;

@Column(name = "author", nullable = false)
private String author;
Comment on lines +35 to +36
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify database migration for the new non-nullable author field.

Adding a non-nullable field to an existing entity may cause issues if there are existing records in the database. Ensure that a proper database migration script is in place to handle existing data.


🏁 Script executed:

#!/bin/bash
# Check if there are any database migration files for the author field
fd -e sql -e xml migration | xargs grep -l "author\|notice" | head -10

Length of output: 72


To locate any existing migration scripts, let’s list all SQL and XML files in the repo:


🏁 Script executed:

#!/bin/bash
# List all SQL and XML files to check for migration scripts
fd -e sql -e xml

Length of output: 56


Add a database migration for the new non-nullable author field

I didn’t find any SQL or XML migration scripts in the repo, so you need to:

  • Create a migration (Flyway, Liquibase, etc.) to
    • Add the author column to the notice table
    • Backfill existing records (e.g. with a default or placeholder)
    • Alter the column to NOT NULL
  • Place it under your migrations folder (e.g. src/main/resources/db/migration) with a proper versioned filename (e.g. V2__add_author_to_notice.sql)
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/domain/notice/NoticeJpaEntity.java at
lines 35-36, you added a new non-nullable 'author' field but did not create a
corresponding database migration. To fix this, create a new migration script
(e.g., Flyway SQL) in the migrations folder (like
src/main/resources/db/migration) with a versioned filename such as
V2__add_author_to_notice.sql. The script should add the 'author' column to the
'notice' table, backfill existing rows with a default or placeholder value, and
then alter the column to be NOT NULL to match the entity definition.


@Builder
public NoticeJpaEntity(
final String title,
final String content,
final Long userId
final Long userId,
final String author
) {
this.title = title;
this.content = content;
this.userId = userId;
this.author = author;
}

public void update(
final String title,
final String content
final String content,
final String author
) {
this.title = title;
this.content = content;
this.author = author;
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package life.mosu.mosuserver.infra.property;

import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Data;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Significant architectural change from immutable to mutable design.

The class has been changed from an immutable configuration properties class to a mutable Spring component. This introduces several concerns:

  1. Thread Safety: The field is no longer final, making the class potentially unsafe for concurrent access
  2. Design Intent: Configuration properties are typically immutable by design
  3. Dependency Injection: The change from constructor injection to field injection may impact testability

Please verify that this change is intentional and consider the thread safety implications, especially since this class is used in services that may be accessed concurrently.

Also applies to: 7-7, 9-10


🏁 Script executed:

#!/bin/bash
# Search for all usages of S3Properties to understand the impact
rg -A 3 -B 3 "S3Properties"

Length of output: 12305


Use constructor binding for S3Properties to enforce immutability and thread-safety

The class is now a Spring singleton bound via setter injection (Lombok’s @DaTa + non-final field), which makes it mutable at runtime. Configuration properties are safer when immutable:

• File: src/main/java/life/mosu/mosuserver/infra/property/S3Properties.java
– Remove Lombok’s @DaTa; use @Getter (or @value) to eliminate setters
– Change private int presignedUrlExpirationMinutes;private final int presignedUrlExpirationMinutes;
– Add Spring Boot’s @ConstructorBinding on the class (requires Spring Boot 2.2+)
– Provide a single constructor and remove any no-args constructor or setters

If you prefer setter binding, please confirm that no code invokes setters at runtime and document this choice to avoid unexpected mutations.

This will restore immutability, preserve thread safety for your singleton bean, and align with best practices for configuration properties.

🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/infra/property/S3Properties.java at line
4, replace Lombok's @Data annotation with @Getter or @Value to remove setters
and enforce immutability. Change the field presignedUrlExpirationMinutes to be
final. Add Spring Boot's @ConstructorBinding annotation to the class and provide
a single constructor initializing all fields, removing any no-args constructor
or setters to ensure the class is immutable and thread-safe as a configuration
properties singleton.

import lombok.extern.slf4j.Slf4j;
Comment on lines +4 to 5

Choose a reason for hiding this comment

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

medium

Consider using @Getter and @Setter instead of @Data to have more control over which methods are generated. @Data generates equals(), hashCode(), and toString() methods, which might not be desirable for configuration properties classes.

Suggested change
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Getter
@Setter
@Component

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Getter
@RequiredArgsConstructor
@Data
@Component
@ConfigurationProperties(prefix = "aws.s3")
@Slf4j
public class S3Properties {

private final int presignedUrlExpirationMinutes;
private int presignedUrlExpirationMinutes;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider preserving immutability for configuration properties.

The change from private final int to private int removes the immutability guarantee. Configuration properties are typically immutable to prevent accidental modification and ensure thread safety.

Consider reverting to the immutable design pattern:

-@Data
-@Component
+@Getter
+@RequiredArgsConstructor
@ConfigurationProperties(prefix = "aws.s3")
@Slf4j
public class S3Properties {

-    private int presignedUrlExpirationMinutes;
+    private final int presignedUrlExpirationMinutes;

If Spring component management is required, consider using @ConfigurationProperties with @EnableConfigurationProperties instead.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/infra/property/S3Properties.java at line
15, the field presignedUrlExpirationMinutes was changed from final to non-final,
removing immutability. Revert this field to be private final to preserve
immutability and thread safety. If you need Spring to manage this configuration
property, use @ConfigurationProperties on the class and enable it with
@EnableConfigurationProperties in your configuration, rather than making the
field mutable.


@PostConstruct
public void init() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package life.mosu.mosuserver.presentation.faq;

import jakarta.validation.Valid;
import java.util.List;
import life.mosu.mosuserver.application.faq.FaqService;
import life.mosu.mosuserver.global.util.ApiResponseWrapper;
import life.mosu.mosuserver.presentation.faq.dto.FaqCreateRequest;
import life.mosu.mosuserver.presentation.faq.dto.FaqResponse;
import life.mosu.mosuserver.presentation.faq.dto.FaqUpdateRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -21,13 +24,15 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/faq")
public class FaqController {
public class FaqController implements FaqControllerDocs {

private final FaqService faqService;

//TODO: 관리자 권한 체크 추가
@PostMapping
public ResponseEntity<ApiResponseWrapper<Void>> create(@RequestBody FaqCreateRequest request) {
// @PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Address the commented security annotations.

The @PreAuthorize annotations are commented out across multiple endpoints. This represents a security concern that should be addressed.

Please verify if these security annotations should be enabled or if there's a planned timeline for implementing proper authorization.

Also applies to: 57-57, 68-68


🏁 Script executed:

#!/bin/bash
# Search for other security patterns in the codebase
rg -A 3 -B 3 "PreAuthorize.*ADMIN"

Length of output: 6548


Enable security checks on admin endpoints

The @PreAuthorize("isAuthenticated() and hasRole('ADMIN')") annotations are currently commented out on several critical endpoints, leaving them unprotected. Please re-enable these annotations or implement an equivalent authorization mechanism for the following methods:

  • FaqController (src/main/java/life/mosu/mosuserver/presentation/faq/FaqController.java):
    • createFaq (formerly at line 33)
    • updateFaq (formerly at line 57)
    • deleteFaq (formerly at line 68)

  • AdminController (src/main/java/life/mosu/mosuserver/presentation/admin/AdminController.java):
    • getStudents
    • downloadStudentInfo
    • getLunchCounts
    • getApplications
    • downloadApplicationInfo

If you’re planning to secure these via another approach (e.g., custom filter, AOP interceptor), please outline the timeline or provide details of the planned solution.

🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/presentation/faq/FaqController.java at
lines 33, 57, and 68, the @PreAuthorize("isAuthenticated() and
hasRole('ADMIN')") annotations are commented out, leaving admin endpoints
unprotected. To fix this, uncomment these annotations on the createFaq,
updateFaq, and deleteFaq methods to enforce security checks ensuring only
authenticated users with the ADMIN role can access them. If using another
security mechanism, provide details or timeline for its implementation.

public ResponseEntity<ApiResponseWrapper<Void>> createFaq(
@Valid @RequestBody FaqCreateRequest request) {
faqService.createFaq(request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.CREATED, "게시글 등록 성공"));
Comment on lines +34 to 37

Choose a reason for hiding this comment

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

high

The createFaq method returns ResponseEntity.ok(), which results in a 200 OK HTTP status. For resource creation, the standard practice is to return a 201 Created status. Using ResponseEntity.status(HttpStatus.CREATED) will align the implementation with REST conventions and the Swagger documentation.

return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponseWrapper.success(HttpStatus.CREATED, "게시글 등록 성공"));

}
Expand All @@ -41,9 +46,27 @@ public ResponseEntity<ApiResponseWrapper<List<FaqResponse>>> getFaqs(
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "게시글 조회 성공", responses));
}

@GetMapping("/{faqId}")
public ResponseEntity<ApiResponseWrapper<FaqResponse>> getFaqDetail(
@PathVariable Long faqId) {
FaqResponse faq = faqService.getFaqDetail(faqId);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "게시글 상세 조회 성공", faq));
}

@PutMapping("/{faqId}")
// @PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> updateFaq(
@PathVariable Long faqId,
@Valid @RequestBody FaqUpdateRequest request
) {
faqService.update(request, faqId);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "게시글 수정 성공"));
}

//TODO: 관리자 권한 체크 추가
@DeleteMapping("/{faqId}")
public ResponseEntity<ApiResponseWrapper<Void>> delete(@PathVariable Long faqId) {
// @PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> deleteFaq(@PathVariable Long faqId) {
faqService.deleteFaq(faqId);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "게시글 삭제 성공"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package life.mosu.mosuserver.presentation.faq;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import life.mosu.mosuserver.global.util.ApiResponseWrapper;
import life.mosu.mosuserver.presentation.faq.dto.FaqCreateRequest;
import life.mosu.mosuserver.presentation.faq.dto.FaqResponse;
import life.mosu.mosuserver.presentation.faq.dto.FaqUpdateRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "Faq API", description = "FAQ 관련 API 명세")
public interface FaqControllerDocs {

@Operation(summary = "FAQ 등록", description = "관리자가 새로운 FAQ를 등록합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "FAQ 등록 성공")
})
ResponseEntity<ApiResponseWrapper<Void>> createFaq(
@Parameter(description = "FAQ 등록 요청 데이터") @RequestBody @Valid FaqCreateRequest request
);

@Operation(summary = "FAQ 목록 조회", description = "전체 FAQ 목록을 페이징하여 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "FAQ 목록 조회 성공",
content = @Content(mediaType = "application/json",
array = @ArraySchema(schema = @Schema(implementation = FaqResponse.class))
)
)
Comment on lines +35 to +40

Choose a reason for hiding this comment

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

medium

The @ApiResponse for getFaqs seems to incorrectly describe the response body. The array attribute suggests the response is a direct JSON array of FaqResponse objects. However, the actual return type is ResponseEntity<ApiResponseWrapper<List<FaqResponse>>>. This will generate misleading Swagger documentation. To fix this, describe the ApiResponseWrapper object itself.


})
ResponseEntity<ApiResponseWrapper<List<FaqResponse>>> getFaqs(
@Parameter(name = "page", description = "페이지 번호", in = ParameterIn.QUERY)
@RequestParam(defaultValue = "0") int page,

@Parameter(name = "size", description = "페이지 크기", in = ParameterIn.QUERY)
@RequestParam(defaultValue = "10") int size
);

@Operation(summary = "FAQ 상세 조회", description = "FAQ ID를 기반으로 상세 내용을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "FAQ 상세 조회 성공")
})
ResponseEntity<ApiResponseWrapper<FaqResponse>> getFaqDetail(
@Parameter(name = "faqId", description = "FAQ ID", in = ParameterIn.PATH)
@PathVariable Long faqId
);

@Operation(summary = "FAQ 삭제", description = "FAQ ID를 기반으로 게시글을 삭제합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "FAQ 삭제 성공")
})
ResponseEntity<ApiResponseWrapper<Void>> deleteFaq(
@Parameter(name = "faqId", description = "삭제할 FAQ ID", in = ParameterIn.PATH)
@PathVariable Long faqId
);

@Operation(summary = "FAQ 수정", description = "기존 FAQ 내용을 수정합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "FAQ 수정 성공")
})
ResponseEntity<ApiResponseWrapper<Void>> updateFaq(
@Parameter(name = "faqId", description = "수정할 FAQ ID", in = ParameterIn.PATH)
@PathVariable Long faqId,

@Parameter(description = "FAQ 수정 요청 데이터")
@RequestBody @Valid FaqUpdateRequest request
);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
package life.mosu.mosuserver.presentation.faq.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import life.mosu.mosuserver.domain.faq.FaqJpaEntity;
import life.mosu.mosuserver.global.util.FileRequest;

public record FaqCreateRequest(

@Schema(description = "FAQ 질문", example = "서비스 이용에 대해 궁금합니다.")
@NotNull String question,

@Schema(description = "FAQ 답변", example = "서비스는 로그인 후 사용 가능합니다.")
@NotNull String answer,

@Schema(description = "작성자 이름", example = "관리자")
@NotNull String author,

@Schema(description = "작성자 ID (추후 토큰에서 추출 예정)", example = "1")
Long userId,

@Schema(description = "첨부파일 리스트")
List<FileRequest> attachments

) {
Expand All @@ -18,9 +29,8 @@ public FaqJpaEntity toEntity() {
return FaqJpaEntity.builder()
.question(question)
.answer(answer)
.author(author)
.userId(userId)
.build();
}


}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
package life.mosu.mosuserver.presentation.faq.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import life.mosu.mosuserver.domain.faq.FaqJpaEntity;

public record FaqResponse(
Long id,
String title,
String content,
List<AttachmentResponse> attachments
@Schema(description = "FAQ ID") Long id,
@Schema(description = "질문") String question,
@Schema(description = "답변") String answer,
@Schema(description = "작성 일자 (yyyy-MM-dd)") String createdAt,
@Schema(description = "첨부파일 리스트") List<AttachmentResponse> attachments
) {

public static FaqResponse of(FaqJpaEntity faq, List<AttachmentResponse> attachments) {
return new FaqResponse(
faq.getId(),
faq.getQuestion(),
faq.getAnswer(),
attachments
faq.getId(),
faq.getQuestion(),
faq.getAnswer(),
faq.getCreatedAt().substring(0, 10), // 일자만 반환

Choose a reason for hiding this comment

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

medium

Using substring(0, 10) to format the date is fragile. If the format of the string from getCreatedAt() changes, this could lead to a StringIndexOutOfBoundsException. A more robust approach would be to parse the string into a Temporal type and then format it to the desired yyyy-MM-dd format using a DateTimeFormatter.

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fragile date extraction logic - consider using proper date formatting.

The substring(0, 10) approach assumes a specific string format and could break if the date format changes.

Recommended improvement:

-                faq.getCreatedAt().substring(0, 10), // 일자만 반환
+                faq.getCreatedAt().toLocalDate().toString(), // 안전한 날짜 추출

Or if getCreatedAt() returns a string, consider using DateTimeFormatter:

-                faq.getCreatedAt().substring(0, 10), // 일자만 반환
+                LocalDateTime.parse(faq.getCreatedAt()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/presentation/faq/dto/FaqResponse.java at
line 20, the code uses substring(0, 10) on the date string returned by
getCreatedAt(), which is fragile and format-dependent. To fix this, parse the
date string into a LocalDate or LocalDateTime object using an appropriate
formatter, then format it back to a string with DateTimeFormatter to reliably
extract the date portion. This ensures the code handles date formats robustly
and avoids errors if the input format changes.

attachments
);
}

public record AttachmentResponse(String fileName, String url) {}
public record AttachmentResponse(
@Schema(description = "파일명") String fileName,
@Schema(description = "파일 URL") String url,
@Schema(description = "S3 키") String s3Key
) {

}
}
Loading