Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ public List<InquiryDetailResponse.AttachmentDetailResponse> toAttachmentResponse
.toList();
}

@Override
public void updateAttachment(
List<FileRequest> requests,
InquiryAnswerJpaEntity answerEntity
) {
deleteAttachment(answerEntity);
createAttachment(requests, answerEntity);
}

private InquiryDetailResponse.AttachmentResponse createAttachResponse(
InquiryAnswerAttachmentEntity attachment) {
String preSignedUrl = s3Service.getPreSignedUrl(attachment.getS3Key());
Expand All @@ -74,4 +83,5 @@ private InquiryDetailResponse.AttachmentDetailResponse createAttachDetailRespons
);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import life.mosu.mosuserver.domain.inquiry.repository.InquiryJpaRepository;
import life.mosu.mosuserver.domain.inquiryAnswer.entity.InquiryAnswerJpaEntity;
import life.mosu.mosuserver.domain.inquiryAnswer.repository.InquiryAnswerJpaRepository;
import life.mosu.mosuserver.domain.user.entity.UserJpaEntity;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerRequest;
Expand All @@ -28,14 +29,14 @@ public class InquiryAnswerService {
private final InquiryAnswerTxService eventTxService;

@Transactional
public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) {
public void createInquiryAnswer(Long postId, InquiryAnswerRequest request, UserJpaEntity user) {
isAnswerAlreadyRegister(postId);
InquiryJpaEntity inquiryEntity = getInquiry(postId);
Long userId = inquiryEntity.getUserId();

try {
InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.save(
request.toEntity(postId));
request.toEntity(postId, user));

answerAttachmentService.createAttachment(request.attachments(), answerEntity);
inquiryEntity.updateStatusToComplete();
Expand All @@ -51,12 +52,12 @@ public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) {
@Transactional
public void deleteInquiryAnswer(Long postId) {
InquiryJpaEntity inquiryEntity = getInquiry(postId);

InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.findByInquiryId(postId)
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.INQUIRY_ANSWER_NOT_FOUND));

inquiryAnswerJpaRepository.delete(answerEntity);
inquiryEntity.updateStatusToPending();
answerAttachmentService.deleteAttachment(answerEntity);
}

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
Expand All @@ -71,17 +72,14 @@ public InquiryDetailResponse.InquiryAnswerDetailResponse getInquiryAnswerDetail(
}

@Transactional
public void updateInquiryAnswer(Long postId, InquiryAnswerUpdateRequest request) {
InquiryJpaEntity inquiryEntity = getInquiry(postId);

public void updateInquiryAnswer(Long postId, InquiryAnswerUpdateRequest request, UserJpaEntity user) {
InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.findByInquiryId(postId)
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.INQUIRY_ANSWER_NOT_FOUND));

answerEntity.update(request.title(), request.content());
answerEntity.update(request.title(), request.content(), user.getName());
inquiryAnswerJpaRepository.save(answerEntity);

answerAttachmentService.deleteAttachment(answerEntity);
answerAttachmentService.createAttachment(request.attachments(), answerEntity);
answerAttachmentService.updateAttachment(request.attachments(), answerEntity);
}

private InquiryJpaEntity getInquiry(Long postId) {
Expand All @@ -96,4 +94,6 @@ private void isAnswerAlreadyRegister(Long postId) {
}




}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ public void deleteAttachment(InquiryJpaEntity entity) {
inquiryAttachmentJpaRepository.deleteAll(attachments);
}

@Override
public void updateAttachment(
List<FileRequest> requests,
InquiryJpaEntity inquiryEntity
) {
deleteAttachment(inquiryEntity);
createAttachment(requests, inquiryEntity);
}


public List<InquiryDetailResponse.AttachmentDetailResponse> toAttachmentResponses(
InquiryJpaEntity inquiry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,9 @@ public void deleteInquiry(UserJpaEntity user, Long postId) {
InquiryJpaEntity inquiry = getInquiry(postId);
hasPermission(inquiry.getUserId(), user);

inquiryAnswerJpaRepository.findByInquiryId(postId).ifPresent(answer -> {
inquiryAnswerService.deleteInquiryAnswer(postId);
});
inquiryAnswerService.deleteInquiryAnswer(postId);
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 that deleteInquiryAnswer handles missing answers gracefully.

The unconditional call to deleteInquiryAnswer simplifies the deletion logic, but ensure that this method can handle cases where no inquiry answer exists without throwing exceptions.


🏁 Script executed:

#!/bin/bash
# Description: Check if deleteInquiryAnswer handles missing answers gracefully
# Expected: Method should handle cases where no answer exists

ast-grep --pattern 'deleteInquiryAnswer($$$) {
  $$$
}'

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Locate the InquiryAnswerService class and the implementation of deleteInquiryAnswer
rg -n "class InquiryAnswerService" -n src/main/java
rg -n "deleteInquiryAnswer" -n src/main/java

Length of output: 950


Further inspecting deleteInquiryAnswer implementation to confirm it handles missing entries without exceptions.


🏁 Script executed:

#!/bin/bash
# Show implementation of deleteInquiryAnswer with surrounding context
rg -n -A20 "public void deleteInquiryAnswer" src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java

Length of output: 1161


Ensure deleteInquiryAnswer handles missing answers without throwing

The current implementation in InquiryAnswerService.java (lines 54–56) will throw a CustomRuntimeException when no answer exists, causing deleteInquiry to fail for inquiries that simply have no answers. To make deletion idempotent, update the method to only delete if an answer is found:

• File: src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java
• Method: public void deleteInquiryAnswer(Long postId)

Suggested change:

-    InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.findByInquiryId(postId)
-            .orElseThrow(() -> new CustomRuntimeException(ErrorCode.INQUIRY_ANSWER_NOT_FOUND));
-
-    inquiryAnswerJpaRepository.delete(answerEntity);
-    inquiryEntity.updateStatusToPending();
-    answerAttachmentService.deleteAttachment(answerEntity);
+    inquiryAnswerJpaRepository.findByInquiryId(postId)
+        .ifPresent(answerEntity -> {
+            inquiryAnswerJpaRepository.delete(answerEntity);
+            inquiryEntity.updateStatusToPending();
+            answerAttachmentService.deleteAttachment(answerEntity);
+        });

This ensures that deleting an inquiry without an existing answer will not throw an exception but simply no-op the answer cleanup.

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java
around lines 54 to 56, the method deleteInquiryAnswer currently throws a
CustomRuntimeException if no answer exists for the given postId. To make
deletion idempotent and avoid exceptions when no answer is found, modify
deleteInquiryAnswer to first check if an answer exists for the postId and only
perform the delete operation if it does. If no answer is found, simply return
without throwing an exception.


// inquiryAttachmentService.deleteAttachment(inquiry);
inquiryAttachmentService.deleteAttachment(inquiry);
inquiryJpaRepository.delete(inquiry);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,14 @@ public void deleteAttachment(NoticeJpaEntity entity) {
noticeAttachmentJpaRepository.deleteAll(attachments);
}

// public List<NoticeResponse.AttachmentResponse> toAttachmentResponses(NoticeJpaEntity notice) {
//
// List<NoticeAttachmentJpaEntity> attachments = noticeAttachmentJpaRepository.findAllByNoticeId(
// notice.getId());
//
// return attachments.stream()
// .map(attachment -> new NoticeResponse.AttachmentResponse(
// attachment.getFileName(),
// fileUrl(attachment.getS3Key())
// ))
// .toList();
// }
@Override
public void updateAttachment(
List<FileRequest> requests,
NoticeJpaEntity noticeEntity
) {
deleteAttachment(noticeEntity);
createAttachment(requests, noticeEntity);
}

public List<NoticeDetailResponse.AttachmentDetailResponse> toDetailAttResponses(
NoticeJpaEntity notice) {
Expand All @@ -70,7 +66,7 @@ public List<NoticeDetailResponse.AttachmentDetailResponse> toDetailAttResponses(
}

private String fileUrl(String s3Key) {
return s3Service.getPreSignedUrl(s3Key);
return s3Service.getPublicUrl(s3Key);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;
import life.mosu.mosuserver.domain.notice.entity.NoticeJpaEntity;
import life.mosu.mosuserver.domain.notice.repository.NoticeJpaRepository;
import life.mosu.mosuserver.domain.user.entity.UserJpaEntity;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.presentation.notice.dto.NoticeCreateRequest;
Expand All @@ -28,8 +29,8 @@ public class NoticeService {
private final NoticeAttachmentService attachmentService;

@Transactional
public void createNotice(NoticeCreateRequest request) {
NoticeJpaEntity noticeEntity = noticeJpaRepository.save(request.toEntity());
public void createNotice(NoticeCreateRequest request, UserJpaEntity user) {
NoticeJpaEntity noticeEntity = noticeJpaRepository.save(request.toEntity(user));
attachmentService.createAttachment(request.attachments(), noticeEntity);
}

Expand All @@ -45,24 +46,23 @@ public List<NoticeResponse> getNotices(int page, int size) {

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public NoticeDetailResponse getNoticeDetail(Long noticeId) {
NoticeJpaEntity notice = getNoticeOrThrow(noticeId);
NoticeJpaEntity notice = getNotice(noticeId);

return toNoticeDetailResponse(notice);
}

@Transactional
public void deleteNotice(Long noticeId) {
NoticeJpaEntity noticeEntity = getNoticeOrThrow(noticeId);
NoticeJpaEntity noticeEntity = getNotice(noticeId);
noticeJpaRepository.delete(noticeEntity);
}

@Transactional
public void updateNotice(Long noticeId, NoticeUpdateRequest request) {
NoticeJpaEntity noticeEntity = getNoticeOrThrow(noticeId);
public void updateNotice(Long noticeId, NoticeUpdateRequest request, UserJpaEntity user) {
NoticeJpaEntity noticeEntity = getNotice(noticeId);

noticeEntity.update(request.title(), request.content(), request.author());
attachmentService.deleteAttachment(noticeEntity);
attachmentService.createAttachment(request.attachments(), noticeEntity);
noticeEntity.update(request.title(), request.content(), user.getName());
attachmentService.updateAttachment(request.attachments(), noticeEntity);
}

private NoticeResponse toNoticeResponse(NoticeJpaEntity notice) {
Expand All @@ -77,7 +77,7 @@ private NoticeDetailResponse toNoticeDetailResponse(NoticeJpaEntity notice) {
);
}

private NoticeJpaEntity getNoticeOrThrow(Long noticeId) {
private NoticeJpaEntity getNotice(Long noticeId) {
return noticeJpaRepository.findById(noticeId)
.orElseThrow(() -> new CustomRuntimeException(ErrorCode.NOTICE_NOT_FOUND));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ public class InquiryJpaEntity extends BaseTimeEntity {
@Column(name = "inquiry_id", nullable = false)
private Long id;

@Column(name = "title", nullable = false)
@Column(name = "title", nullable = false, length = 300)
private String title;

@Column(name = "content", nullable = false)
@Column(name = "content", nullable = false, length = 1000)
private String content;

@Column(name = "user_id", nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ public class InquiryAnswerJpaEntity extends BaseTimeEntity {
@Column(name = "inquiry_answer_id", nullable = false)
private Long id;

@Column(name = "title", nullable = false, length = 3000)
@Column(name = "title", nullable = false, length = 300)
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 the title length reduction impact.

The title column length was reduced from 3000 to 300 characters. This is a significant reduction that could cause data truncation if existing records exceed the new limit.


🏁 Script executed:

#!/bin/bash
# Description: Check for existing inquiry answer titles that exceed 300 characters
# Expected: Should return any titles longer than 300 characters that would be truncated

# Search for the table structure and any existing data concerns
rg -A 5 -B 5 "inquiry_answer" --type sql

# Look for any migration files that handle this change
fd -e sql -e java | xargs rg -l "ALTER.*inquiry_answer.*title|title.*300"

Length of output: 435


Add a DB migration for title length reduction

We didn’t detect any SQL migration altering the inquiry_answer.title column from 3000 → 300 characters. Without a dedicated Flyway/Liquibase (or equivalent) script, existing records > 300 chars will either be truncated or cause errors in production.

Suggested fixes:

  • Add a migration in resources/db/migration/... (or your project’s convention) to alter the column, for example:
    ALTER TABLE inquiry_answer
      ALTER COLUMN title TYPE VARCHAR(300);
  • Audit or migrate existing data so no titles exceed 300 characters (or decide on truncation strategy).
  • Confirm your CI/integration tests cover reads/writes around the 300-char boundary.
  • Reference the JPA mapping in src/main/java/life/mosu/mosuserver/domain/inquiryAnswer/entity/InquiryAnswerJpaEntity.java (line 28).
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/domain/inquiryAnswer/entity/InquiryAnswerJpaEntity.java
at line 28, the title column length was reduced from 3000 to 300 characters in
the JPA entity but no corresponding database migration exists. To fix this,
create a new SQL migration script in the resources/db/migration directory that
alters the inquiry_answer.title column to VARCHAR(300). Additionally, audit
existing data to ensure no titles exceed 300 characters or implement a
truncation strategy. Finally, verify that your tests cover scenarios with title
lengths near the 300-character limit.

private String title;

@Column(name = "content", nullable = false)
@Column(name = "content", nullable = false, length = 1000)
private String content;

@Column(name = "inquiry_id", nullable = false)
private Long inquiryId;

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

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

Expand All @@ -42,16 +45,20 @@ public InquiryAnswerJpaEntity(
final String title,
final String content,
final Long inquiryId,
final Long userId
final Long userId,
final String author

) {
this.title = title;
this.content = content;
this.inquiryId = inquiryId;
this.userId = userId;
this.author = author;
}

public void update(final String title, final String content) {
public void update(final String title, 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,6 +1,7 @@
package life.mosu.mosuserver.global.exception;

import jakarta.persistence.EntityNotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Map;
import life.mosu.mosuserver.infra.notify.NotifyClientAdapter;
Expand All @@ -15,6 +16,8 @@
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.reactive.resource.NoResourceFoundException;

@Slf4j
@RestControllerAdvice
Expand Down Expand Up @@ -123,6 +126,31 @@ public ResponseEntity<ErrorResponse> handleHttpMessageNotReadableException(
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}

@ExceptionHandler(NoResourceFoundException.class )
public ResponseEntity<ErrorResponse> handleNotFound(Exception ex) {
notifyIfNeeded(ex);

ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.NOT_FOUND.value())
.message("요청하신 리소스를 찾을 수 없습니다.")
.errors(ex.getMessage())
.build();

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
notifyIfNeeded(ex);
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.BAD_REQUEST.value())
.code("TYPE_MISMATCH")
.message("요청 파라미터 타입이 올바르지 않습니다.")
.build();

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
notifyIfNeeded(ex);
Expand All @@ -137,7 +165,9 @@ public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
}

@ExceptionHandler(CustomRuntimeException.class)
public ResponseEntity<ErrorResponse> handleCustomRuntimeException(CustomRuntimeException ex) {
public ResponseEntity<ErrorResponse> handleCustomRuntimeException(
CustomRuntimeException ex
) {
notifyIfNeeded(ex);

ErrorResponse response = ErrorResponse.builder()
Expand All @@ -152,7 +182,7 @@ public ResponseEntity<ErrorResponse> handleCustomRuntimeException(CustomRuntimeE
private void notifyIfNeeded(Exception ex) {
try {
DiscordExceptionNotifyEventRequest request = DiscordExceptionNotifyEventRequest.of(
ex.getCause().toString(),
ex.getCause() != null ? ex.getCause().toString() : "Unknown Cause",
ex.getMessage()
);
notifier.send(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ public interface AttachmentService<E, R> {
void createAttachment(List<R> fileRequests, E entity);

void deleteAttachment(E entity);

void updateAttachment(List<R> fileRequests, E entity);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package life.mosu.mosuserver.presentation.admin;

import life.mosu.mosuserver.application.auth.PrincipalDetails;
import life.mosu.mosuserver.application.inquiry.InquiryAnswerService;
import life.mosu.mosuserver.application.inquiry.InquiryService;
import life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus;
Expand All @@ -15,6 +16,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -50,20 +52,22 @@ public ResponseEntity<ApiResponseWrapper<Page<InquiryResponse>>> getInquiryList(
@PostMapping("/{postId}/answer")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> inquiryAnswer(
@AuthenticationPrincipal PrincipalDetails principalDetails,
@PathVariable Long postId,
@RequestBody InquiryAnswerRequest request
) {
inquiryAnswerService.createInquiryAnswer(postId, request);
inquiryAnswerService.createInquiryAnswer(postId, request, principalDetails.user());
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "답변 등록 성공"));
}

@PutMapping("/{postId}/answer")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> updateInquiryAnswer(
@AuthenticationPrincipal PrincipalDetails principalDetails,
@PathVariable Long postId,
@RequestBody InquiryAnswerUpdateRequest request
) {
inquiryAnswerService.updateInquiryAnswer(postId, request);
inquiryAnswerService.updateInquiryAnswer(postId, request, principalDetails.user());
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "답변 수정 성공"));
}

Expand Down
Loading