From cbde36254e3e2034820f3c5b5b81c83a4c11eb00 Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Fri, 8 Aug 2025 06:08:13 +0900 Subject: [PATCH 01/16] refactor: update application configuration files for logging and environment profiles --- src/main/resources/application-local.yml | 9 +++++++++ src/main/resources/application-prod.yml | 14 ++++++++++++++ src/main/resources/application-test.yml | 6 ++++++ src/main/resources/application.yml | 17 ++--------------- 4 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/main/resources/application-local.yml create mode 100644 src/main/resources/application-prod.yml create mode 100644 src/main/resources/application-test.yml diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 00000000..74e2c615 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,9 @@ +logging: + file: + path: ./logs + name: app.log + level: + root: TRACE + +discord: + base-url: "" \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..9e21cd1d --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,14 @@ +logging: + file: + path: ./logs + name: app.log + level: + root: INFO + +toss: + secret-key: ${TOSS_SECRET_KEY} + api: + base-url: https://api.tosspayments.com/v1 + +discord: + base-url: ${DISCORD_URL} \ No newline at end of file diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 00000000..d1630808 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,6 @@ +logging: + file: + path: ./logs + name: app.log + level: + root: TRACE diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0deb7721..739f5e3f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -71,6 +71,8 @@ spring: view: prefix: /WEB-INF/views/ suffix: .jsp + profiles: + active: ${SPRING_PROFILES_ACTIVE} management: endpoints: @@ -86,18 +88,6 @@ aws: secret-key: ${AWS_SECRET_KEY} pre-signed-url-expiration-minutes: ${S3_PRESIGNED_URL_EXPIRATION_MINUTES} -logging: - file: - path: ./logs - name: app.log - level: - root: INFO - - -toss: - secret-key: ${TOSS_SECRET_KEY} - api: - base-url: https://api.tosspayments.com/v1 alimtalk: user-id: ${ALIMTALK_USER_ID} @@ -107,6 +97,3 @@ alimtalk: kakao: channel-id: ${KAKAO_CHANNEL_ID} - -discord: - base-url: ${DISCORD_URL} \ No newline at end of file From fe516dc4dfb359ce66e9191d344ace51891b9695 Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Fri, 8 Aug 2025 06:10:15 +0900 Subject: [PATCH 02/16] refactor: update application configuration files for logging and environment profiles --- .../application/inquiry/InquiryService.java | 12 ++++++++++++ .../inquiry/entity/InquiryJpaEntity.java | 6 ++++++ .../inquiry/InquiryController.java | 12 ++++++++++++ .../inquiry/dto/InquiryUpdateRequest.java | 18 ++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryUpdateRequest.java diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java index 965f5f42..b845fafa 100644 --- a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java @@ -1,5 +1,6 @@ package life.mosu.mosuserver.application.inquiry; +import jakarta.validation.Valid; import life.mosu.mosuserver.domain.inquiry.entity.InquiryJpaEntity; import life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus; import life.mosu.mosuserver.domain.inquiry.repository.InquiryJpaRepository; @@ -13,6 +14,7 @@ import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse.InquiryAnswerDetailResponse; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryResponse; +import life.mosu.mosuserver.presentation.inquiry.dto.InquiryUpdateRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -93,4 +95,14 @@ private void hasPermission(Long postUserId, UserJpaEntity user) { throw new CustomRuntimeException(ErrorCode.USER_NOT_ACCESS_FORBIDDEN); } } + + public void updateInquiry(UserJpaEntity user, InquiryUpdateRequest request, Long postId) { + InquiryJpaEntity inquiry = getInquiry(postId); + hasPermission(inquiry.getUserId(), user); + + inquiry.update(request.title(), request.content(), user.getName()); + inquiryJpaRepository.save(inquiry); + + inquiryAttachmentService.updateAttachment(request.attachments(), inquiry); + } } diff --git a/src/main/java/life/mosu/mosuserver/domain/inquiry/entity/InquiryJpaEntity.java b/src/main/java/life/mosu/mosuserver/domain/inquiry/entity/InquiryJpaEntity.java index 9d56c56e..17b8454e 100644 --- a/src/main/java/life/mosu/mosuserver/domain/inquiry/entity/InquiryJpaEntity.java +++ b/src/main/java/life/mosu/mosuserver/domain/inquiry/entity/InquiryJpaEntity.java @@ -57,4 +57,10 @@ public void updateStatusToComplete() { public void updateStatusToPending() { this.status = InquiryStatus.PENDING; } + + public void update(final String title, final String content, final String author) { + this.title = title; + this.content = content; + this.author = author; + } } diff --git a/src/main/java/life/mosu/mosuserver/presentation/inquiry/InquiryController.java b/src/main/java/life/mosu/mosuserver/presentation/inquiry/InquiryController.java index a34c0d74..f9bff42d 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/inquiry/InquiryController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/inquiry/InquiryController.java @@ -8,6 +8,7 @@ import life.mosu.mosuserver.presentation.inquiry.dto.InquiryCreateRequest; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryResponse; +import life.mosu.mosuserver.presentation.inquiry.dto.InquiryUpdateRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -20,6 +21,7 @@ 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.RestController; @@ -40,6 +42,16 @@ public ResponseEntity> create( return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.CREATED, "질문 등록 성공")); } + @PutMapping("/{postId}") + @PreAuthorize("isAuthenticated() and hasRole('USER')") + public ResponseEntity> update( + @PathVariable Long postId, + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestBody @Valid InquiryUpdateRequest request) { + inquiryService.updateInquiry(principalDetails.user(), request, postId); + return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "질문 수정 성공")); + } + @GetMapping("/my") @PreAuthorize("isAuthenticated() and hasRole('USER')") public ResponseEntity>> getMyInquiries( diff --git a/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryUpdateRequest.java b/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryUpdateRequest.java new file mode 100644 index 00000000..0e0a26cb --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryUpdateRequest.java @@ -0,0 +1,18 @@ +package life.mosu.mosuserver.presentation.inquiry.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; +import life.mosu.mosuserver.presentation.common.FileRequest; + +public record InquiryUpdateRequest( + @Size(max = 100, message = "제목은 최대 300자까지 입력 가능합니다.") + @Schema(description = "문의 제목", example = "서비스 이용 관련 질문입니다.") + @NotNull String title, + + @Size(max = 1000, message = "본문은 최대 1000자까지 입력 가능합니다.") + @Schema(description = "문의 내용", example = "포인트는 어떻게 사용하나요?") + @NotNull String content, + List attachments +) { } From b9e16878769cc35f59034a6438b0937d453ca821 Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Fri, 8 Aug 2025 14:39:28 +0900 Subject: [PATCH 03/16] refactor: add default value for Discord webhook URL and prevent notifications if not configured --- .../mosu/mosuserver/infra/notify/DiscordNotifier.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java b/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java index 8decec77..34298cef 100644 --- a/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java +++ b/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java @@ -1,6 +1,7 @@ package life.mosu.mosuserver.infra.notify; import java.util.Map; +import java.util.Objects; import life.mosu.mosuserver.infra.notify.dto.discord.DiscordExceptionNotifyEventRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,11 +21,16 @@ public class DiscordNotifier implements NotifyClientAdapter Date: Sun, 10 Aug 2025 01:49:29 +0900 Subject: [PATCH 04/16] =?UTF-8?q?MOSU-267=20feat:=20cache=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=A0=20=EB=8F=84=EB=A9=94=EC=9D=B8=EB=93=A4=20enu?= =?UTF-8?q?m=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/caffeine/CacheGroup.java | 33 +++++++++++++++++++ .../mosuserver/domain/caffeine/CacheType.java | 13 ++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java create mode 100644 src/main/java/life/mosu/mosuserver/domain/caffeine/CacheType.java diff --git a/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java new file mode 100644 index 00000000..eaee8b2c --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java @@ -0,0 +1,33 @@ +package life.mosu.mosuserver.domain.caffeine; + +import java.time.Duration; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum CacheGroup { + + NOTICE( + "notice", + Duration.ofMinutes(10), + CacheType.LOCAL + ), + + GLOBAL_ONLY( + "globalOnly", + Duration.ofMinutes(10), + CacheType.GLOBAL + ), + + COMPOSITE_ALL( + "composite", + Duration.ofMinutes(10), + CacheType.COMPOSITE + ); + + + private final String cacheName; + private final Duration expiredAfterWrite; + private final CacheType cacheType; +} diff --git a/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheType.java b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheType.java new file mode 100644 index 00000000..98c4e603 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheType.java @@ -0,0 +1,13 @@ +package life.mosu.mosuserver.domain.caffeine; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum CacheType { + LOCAL("로컬 타입만 적용"), + GLOBAL("분산 캐시만 적용"), + COMPOSITE("로컬 + 분산 캐시 모두 적용"); + + private final String type; + +} \ No newline at end of file From 40f61d0bd20b6d55f465875e5887799a72adb70f Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Sun, 10 Aug 2025 01:50:12 +0900 Subject: [PATCH 05/16] =?UTF-8?q?MOSU-267=20feat:=20LocalCacheManager=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../caffeine/LocalCacheConfig.java | 42 +++++++++++++ .../caffeine/LocalCacheManager.java | 59 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheConfig.java create mode 100644 src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheManager.java diff --git a/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheConfig.java b/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheConfig.java new file mode 100644 index 00000000..44de15be --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheConfig.java @@ -0,0 +1,42 @@ +package life.mosu.mosuserver.application.caffeine; + +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import life.mosu.mosuserver.domain.caffeine.CacheGroup; +import life.mosu.mosuserver.domain.caffeine.CacheType; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@EnableCaching +@Configuration +public class LocalCacheConfig { + + @Bean + public LocalCacheManager localCacheManager() { + List caches = Arrays.stream(CacheGroup.values()) + .filter(g -> g.getCacheType() == CacheType.LOCAL + || g.getCacheType() == CacheType.COMPOSITE) + .map(g -> new CaffeineCache( + g.getCacheName(), + Caffeine.newBuilder() + .recordStats() + .expireAfterWrite(g.getExpiredAfterWrite()) + .build() + )).collect(Collectors.toList()); + + return new LocalCacheManager(caches); + } + + @Bean + @Primary + public CacheManager appCacheManager(LocalCacheManager localCacheManager) { + return localCacheManager; + } +} diff --git a/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheManager.java b/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheManager.java new file mode 100644 index 00000000..e800e56d --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheManager.java @@ -0,0 +1,59 @@ +package life.mosu.mosuserver.application.caffeine; + +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +public class LocalCacheManager implements CacheManager, UpdatableCacheManager { + + private final List caches; + private Map cacheMap = new ConcurrentHashMap<>(); + private volatile Set cacheNames = Collections.emptySet(); + + public LocalCacheManager(List caches) { + this.caches = (caches != null) ? caches : Collections.emptyList(); + } + + @PostConstruct + public void init() { + Set cacheNamesSet = new LinkedHashSet<>(caches.size()); + Map cacheMapTemp = new ConcurrentHashMap<>(16); + + for (Cache cache : caches) { + String name = cache.getName(); + cacheNamesSet.add(name); + cacheMapTemp.put(name, cache); + } + this.cacheMap = cacheMapTemp; + this.cacheNames = cacheNamesSet; + } + + @Override + @Nullable + public Cache getCache(String name) { + return cacheMap.get(name); + } + + @Override + public Collection getCacheNames() { + return cacheNames; + } + + @Override + public void putIfAbsent(Cache cache, String key, Object value) { + Cache localCache = getCache(cache.getName()); + if (localCache != null) { + localCache.putIfAbsent(key, value); + } + } + + +} From c5be4ad5d22ca72f287c0c76fe42d6db3405e417 Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Sun, 10 Aug 2025 16:11:07 +0900 Subject: [PATCH 06/16] =?UTF-8?q?MOSU-267=20fix:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 99ddb470..5d656bc3 100644 --- a/build.gradle +++ b/build.gradle @@ -106,8 +106,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-testcontainers:3.3.5' testImplementation 'org.testcontainers:testcontainers:1.19.3' testImplementation 'org.testcontainers:junit-jupiter:1.19.3' - testImplementation 'org.testcoscntainers:mysql:1.20.0' - + testImplementation 'org.testcontainers:mysql:1.20.0' annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" implementation 'org.apache.commons:commons-pool2:2.12.1' From a0fd7db899abf24506649bd57e0503e3ff3bea0d Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Sun, 10 Aug 2025 16:11:30 +0900 Subject: [PATCH 07/16] =?UTF-8?q?MOSU-267=20feat:=20=EA=B3=B5=EC=A7=80=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=BA=90=EC=8B=B1=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/caffeine/UpdatableCacheManager.java | 8 ++++++++ .../mosuserver/application/inquiry/InquiryService.java | 4 ---- .../mosu/mosuserver/application/notice/NoticeService.java | 8 ++++++++ .../life/mosu/mosuserver/domain/caffeine/CacheGroup.java | 4 ++-- ...ineCacheConfig.java => CaffeineFilterCacheConfig.java} | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 src/main/java/life/mosu/mosuserver/application/caffeine/UpdatableCacheManager.java rename src/main/java/life/mosu/mosuserver/global/config/{CaffeineCacheConfig.java => CaffeineFilterCacheConfig.java} (98%) diff --git a/src/main/java/life/mosu/mosuserver/application/caffeine/UpdatableCacheManager.java b/src/main/java/life/mosu/mosuserver/application/caffeine/UpdatableCacheManager.java new file mode 100644 index 00000000..92939647 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/caffeine/UpdatableCacheManager.java @@ -0,0 +1,8 @@ +package life.mosu.mosuserver.application.caffeine; + +import org.springframework.cache.Cache; + +public interface UpdatableCacheManager { + + void putIfAbsent(Cache cache, String key, Object value); +} diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java index 965f5f42..a6fbdb69 100644 --- a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java @@ -3,10 +3,8 @@ import life.mosu.mosuserver.domain.inquiry.entity.InquiryJpaEntity; import life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus; import life.mosu.mosuserver.domain.inquiry.repository.InquiryJpaRepository; -import life.mosu.mosuserver.domain.inquiryAnswer.repository.InquiryAnswerJpaRepository; import life.mosu.mosuserver.domain.user.entity.UserJpaEntity; import life.mosu.mosuserver.domain.user.entity.UserRole; -import life.mosu.mosuserver.domain.user.repository.UserJpaRepository; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryCreateRequest; @@ -26,11 +24,9 @@ @RequiredArgsConstructor public class InquiryService { - private final UserJpaRepository userJpaRepository; private final InquiryAttachmentService inquiryAttachmentService; private final InquiryJpaRepository inquiryJpaRepository; private final InquiryAnswerService inquiryAnswerService; - private final InquiryAnswerJpaRepository inquiryAnswerJpaRepository; @Transactional public void createInquiry(UserJpaEntity user, InquiryCreateRequest request) { diff --git a/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java b/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java index 4405792c..379df88b 100644 --- a/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java +++ b/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java @@ -12,6 +12,8 @@ import life.mosu.mosuserver.presentation.notice.dto.NoticeUpdateRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -28,12 +30,15 @@ public class NoticeService { private final NoticeJpaRepository noticeJpaRepository; private final NoticeAttachmentService attachmentService; + @CacheEvict(cacheNames = "notice", allEntries = true) @Transactional public void createNotice(NoticeCreateRequest request, UserJpaEntity user) { NoticeJpaEntity noticeEntity = noticeJpaRepository.save(request.toEntity(user)); attachmentService.createAttachment(request.attachments(), noticeEntity); } + @Cacheable(cacheNames = "notice", + key = "'page=' + #page + ',size=' + #size") @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public List getNotices(int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("id")); @@ -44,6 +49,7 @@ public List getNotices(int page, int size) { .toList(); } + @Cacheable(cacheNames = "notice", key = "#noticeId") @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public NoticeDetailResponse getNoticeDetail(Long noticeId) { NoticeJpaEntity notice = getNotice(noticeId); @@ -51,12 +57,14 @@ public NoticeDetailResponse getNoticeDetail(Long noticeId) { return toNoticeDetailResponse(notice); } + @CacheEvict(cacheNames = "notice", allEntries = true) @Transactional public void deleteNotice(Long noticeId) { NoticeJpaEntity noticeEntity = getNotice(noticeId); noticeJpaRepository.delete(noticeEntity); } + @CacheEvict(cacheNames = "notice", allEntries = true) @Transactional public void updateNotice(Long noticeId, NoticeUpdateRequest request, UserJpaEntity user) { NoticeJpaEntity noticeEntity = getNotice(noticeId); diff --git a/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java index eaee8b2c..af1f57e4 100644 --- a/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java +++ b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java @@ -14,8 +14,8 @@ public enum CacheGroup { CacheType.LOCAL ), - GLOBAL_ONLY( - "globalOnly", + INQUIRY( + "inquiry", Duration.ofMinutes(10), CacheType.GLOBAL ), diff --git a/src/main/java/life/mosu/mosuserver/global/config/CaffeineCacheConfig.java b/src/main/java/life/mosu/mosuserver/global/config/CaffeineFilterCacheConfig.java similarity index 98% rename from src/main/java/life/mosu/mosuserver/global/config/CaffeineCacheConfig.java rename to src/main/java/life/mosu/mosuserver/global/config/CaffeineFilterCacheConfig.java index 79d43652..52fab7fa 100644 --- a/src/main/java/life/mosu/mosuserver/global/config/CaffeineCacheConfig.java +++ b/src/main/java/life/mosu/mosuserver/global/config/CaffeineFilterCacheConfig.java @@ -12,7 +12,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class CaffeineCacheConfig { +public class CaffeineFilterCacheConfig { @Bean public Cache ipRequestCountsCache(IpRateLimitingProperties properties) { From febbe32032532a9023c36534c726e569c42d8522 Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Sun, 10 Aug 2025 20:29:50 +0900 Subject: [PATCH 08/16] refactor: remove unnecessary @NotBlank annotation from PhoneNumberPattern --- .../mosu/mosuserver/global/annotation/PhoneNumberPattern.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/life/mosu/mosuserver/global/annotation/PhoneNumberPattern.java b/src/main/java/life/mosu/mosuserver/global/annotation/PhoneNumberPattern.java index 1fa17704..d03eb5f6 100644 --- a/src/main/java/life/mosu/mosuserver/global/annotation/PhoneNumberPattern.java +++ b/src/main/java/life/mosu/mosuserver/global/annotation/PhoneNumberPattern.java @@ -2,7 +2,6 @@ import jakarta.validation.Constraint; import jakarta.validation.Payload; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -13,7 +12,6 @@ regexp = "^01[016789]-\\d{3,4}-\\d{4}$", message = "전화번호 형식은 010-XXXX-XXXX 이어야 합니다." ) -@NotBlank @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = {}) From 03357a6a3dc60991485534dd78502dc4093e61bc Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sun, 10 Aug 2025 21:36:39 +0900 Subject: [PATCH 09/16] feat: reorganize application configuration files and add base profile --- src/main/resources/application-base.yml | 83 +++++++++++++++++++++ src/main/resources/application-local.yml | 17 +++-- src/main/resources/application-prod.yml | 19 ++++- src/main/resources/application-test.yml | 23 +++++- src/main/resources/application.yml | 94 +----------------------- 5 files changed, 135 insertions(+), 101 deletions(-) create mode 100644 src/main/resources/application-base.yml diff --git a/src/main/resources/application-base.yml b/src/main/resources/application-base.yml new file mode 100644 index 00000000..4df54a41 --- /dev/null +++ b/src/main/resources/application-base.yml @@ -0,0 +1,83 @@ +server: + port: ${SPRING_PORT} + servlet: + context-path: ${BASE_PATH} + session: + cookie: + same-site: none + secure: false + error: + include-stacktrace: never + +spring: + devtools: + restart: + enabled: false + mail: + host: ${MAIL_HOST} + port: ${MAIL_PORT} + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + properties: + mail.smtp.debug: true + mail.smtp.connectiontimeout: 1000 + mail.starttls.enable: true + mail.smtp.auth: true + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 15 + minimum-idle: 15 + + servlet: + multipart: + max-file-size: ${MAX_FILE_SIZE} + max-request-size: ${MAX_REQUEST_SIZE} + jpa: + open-in-view: false + show-sql: true + hibernate: + ddl-auto: update + + properties: + hibernate: + show_sql: true + format_sql: true + highlight_sql: true + use_sql_comments: true + jdbc: + time_zone: Asia/Seoul + dialect: org.hibernate.dialect.MySQLDialect + data: + redis: + host: ${REDIS_HOST} + port: ${VELKEY_PORT} + lettuce: + pool: + enabled: true + max-active: 32 + max-idle: 8 + min-idle: 4 + max-wait: 1000 + messages: + basename: messages + encoding: UTF-8 + mvc: + view: + prefix: /WEB-INF/views/ + suffix: .jsp +aws: + s3: + bucket-name: ${AWS_BUCKET_NAME} + region: ${AWS_REGION} + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + pre-signed-url-expiration-minutes: ${S3_PRESIGNED_URL_EXPIRATION_MINUTES} + +toss: + api: + base-url: https://api.tosspayments.com/v1 + diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 74e2c615..3c3dc3fb 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,9 +1,16 @@ logging: - file: - path: ./logs - name: app.log level: root: TRACE - +toss: + secret-key: ${TOSS_SECRET_KEY} discord: - base-url: "" \ No newline at end of file + base-url: "" + +alimtalk: + user-id: ${ALIMTALK_USER_ID} + api-key: ${ALIMTALK_API_KEY} + api: + base-url: ${ALIMTALK_URL} + +kakao: + channel-id: ${KAKAO_CHANNEL_ID} \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 9e21cd1d..b0021cd1 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -5,10 +5,23 @@ logging: level: root: INFO +management: + endpoints: + web: + exposure: + include: "*" toss: secret-key: ${TOSS_SECRET_KEY} - api: - base-url: https://api.tosspayments.com/v1 discord: - base-url: ${DISCORD_URL} \ No newline at end of file + base-url: ${DISCORD_URL} + + +alimtalk: + user-id: ${ALIMTALK_USER_ID} + api-key: ${ALIMTALK_API_KEY} + api: + base-url: ${ALIMTALK_URL} + +kakao: + channel-id: ${KAKAO_CHANNEL_ID} \ No newline at end of file diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index d1630808..7dbd9893 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -3,4 +3,25 @@ logging: path: ./logs name: app.log level: - root: TRACE + root: info + +management: + endpoints: + web: + exposure: + include: "*" +toss: + secret-key: ${TOSS_SECRET_KEY} + +discord: + base-url: "" + + +alimtalk: + user-id: ${ALIMTALK_USER_ID} + api-key: ${ALIMTALK_API_KEY} + api: + base-url: ${ALIMTALK_URL} + +kakao: + channel-id: ${KAKAO_CHANNEL_ID} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 739f5e3f..18ee0f4c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,99 +1,9 @@ -server: - port: ${SPRING_PORT} - servlet: - context-path: ${BASE_PATH} - session: - cookie: - same-site: none - secure: false - error: - include-stacktrace: never - spring: - mail: - host: ${MAIL_HOST} - port: ${MAIL_PORT} - username: ${MAIL_USERNAME} - password: ${MAIL_PASSWORD} - properties: - mail.smtp.debug: true - mail.smtp.connectiontimeout: 1000 - mail.starttls.enable: true - mail.smtp.auth: true config: import: - optional:file:.env[.properties] - security-config.yml - swagger-config.yml - datasource: - url: ${DB_URL} - username: ${DB_USERNAME} - password: ${DB_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver - hikari: - maximum-pool-size: 15 - minimum-idle: 15 - - servlet: - multipart: - max-file-size: ${MAX_FILE_SIZE} - max-request-size: ${MAX_REQUEST_SIZE} - jpa: - open-in-view: false - show-sql: true - hibernate: - ddl-auto: update - - properties: - hibernate: - show_sql: true - format_sql: true - highlight_sql: true - use_sql_comments: true - jdbc: - time_zone: Asia/Seoul - dialect: org.hibernate.dialect.MySQLDialect - data: - redis: - host: ${REDIS_HOST} - port: ${VELKEY_PORT} - lettuce: - pool: - enabled: true - max-active: 32 - max-idle: 8 - min-idle: 4 - max-wait: 1000 - messages: - basename: messages - encoding: UTF-8 - mvc: - view: - prefix: /WEB-INF/views/ - suffix: .jsp profiles: - active: ${SPRING_PROFILES_ACTIVE} - -management: - endpoints: - web: - exposure: - include: "*" - -aws: - s3: - bucket-name: ${AWS_BUCKET_NAME} - region: ${AWS_REGION} - access-key: ${AWS_ACCESS_KEY} - secret-key: ${AWS_SECRET_KEY} - pre-signed-url-expiration-minutes: ${S3_PRESIGNED_URL_EXPIRATION_MINUTES} - - -alimtalk: - user-id: ${ALIMTALK_USER_ID} - api-key: ${ALIMTALK_API_KEY} - api: - base-url: ${ALIMTALK_URL} - -kakao: - channel-id: ${KAKAO_CHANNEL_ID} + active: ${APPLICATION_PROFILE:prod} + include: base \ No newline at end of file From affed2258baad9cbe4030ad5b532aa8e9faf86ad Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sun, 10 Aug 2025 21:36:52 +0900 Subject: [PATCH 10/16] feat: update deployment scripts to use base environment variables and streamline configuration --- .github/workflows/docker-depoly.yaml | 11 +++++++++-- .github/workflows/self-depoly.yaml | 12 ++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-depoly.yaml b/.github/workflows/docker-depoly.yaml index fb544e6e..e0f4ca14 100644 --- a/.github/workflows/docker-depoly.yaml +++ b/.github/workflows/docker-depoly.yaml @@ -22,9 +22,16 @@ jobs: script: | cd /home/ubuntu/mosu - echo "${{ secrets.ENV_BLUE }}" > .env.blue - echo "${{ secrets.ENV_GREEN }}" > .env.green + echo "${{ secrets.ENV_BASE }}" > .env + echo "${{ secrets.ENV_BASE }}" > .env.blue + echo "${{ secrets.ENV_BASE }}" > .env.green + echo "${{ secrets.ENV }}" >> .env + echo "${{ secrets.ENV }}" >> .env.blue + echo "${{ secrets.ENV }}" >> .env.green + + echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env.blue echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env.green + ./deploy.sh diff --git a/.github/workflows/self-depoly.yaml b/.github/workflows/self-depoly.yaml index 2a3e8327..f077bbdc 100644 --- a/.github/workflows/self-depoly.yaml +++ b/.github/workflows/self-depoly.yaml @@ -53,10 +53,18 @@ jobs: run: | cd ~/mosu-server - echo "${{ secrets.TEST_ENV_BLUE }}" > .env.blue - echo "${{ secrets.TEST_ENV_GREEN }}" > .env.green + echo "${{ secrets.ENV_BASE }}" > .env + echo "${{ secrets.ENV_BASE }}" > .env.blue + echo "${{ secrets.ENV_BASE }}" > .env.green + + echo "${{ secrets.ENV_TEST }}" >> .env + echo "${{ secrets.ENV_TEST }}" >> .env.blue + echo "${{ secrets.ENV_TEST }}" >> .env.green + + echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env.blue echo "APP_IMAGE_VERSION=${{ github.sha }}" >> .env.green + sudo docker stop $(sudo docker ps -aq) || true sudo docker rm $(sudo docker ps -aq) || true echo "Stopping all containers..." From 7f0bf734d26adf99769bb652e8c35c9d02bc20b1 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sun, 10 Aug 2025 21:40:06 +0900 Subject: [PATCH 11/16] feat: implement updateInquiry method for modifying existing inquiries --- .../application/inquiry/InquiryService.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java index 1c185dad..addec98a 100644 --- a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java @@ -1,6 +1,5 @@ package life.mosu.mosuserver.application.inquiry; -import jakarta.validation.Valid; import life.mosu.mosuserver.domain.inquiry.entity.InquiryJpaEntity; import life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus; import life.mosu.mosuserver.domain.inquiry.repository.InquiryJpaRepository; @@ -72,6 +71,17 @@ public void deleteInquiry(UserJpaEntity user, Long postId) { inquiryJpaRepository.delete(inquiry); } + @Transactional + public void updateInquiry(UserJpaEntity user, InquiryUpdateRequest request, Long postId) { + InquiryJpaEntity inquiry = getInquiry(postId); + hasPermission(inquiry.getUserId(), user); + + inquiry.update(request.title(), request.content(), user.getName()); + inquiryJpaRepository.save(inquiry); + + inquiryAttachmentService.updateAttachment(request.attachments(), inquiry); + } + private InquiryDetailResponse toInquiryDetailResponse(InquiryJpaEntity inquiry) { InquiryAnswerDetailResponse answer = inquiryAnswerService.getInquiryAnswerDetail( inquiry.getId()); @@ -91,14 +101,4 @@ private void hasPermission(Long postUserId, UserJpaEntity user) { throw new CustomRuntimeException(ErrorCode.USER_NOT_ACCESS_FORBIDDEN); } } - - public void updateInquiry(UserJpaEntity user, InquiryUpdateRequest request, Long postId) { - InquiryJpaEntity inquiry = getInquiry(postId); - hasPermission(inquiry.getUserId(), user); - - inquiry.update(request.title(), request.content(), user.getName()); - inquiryJpaRepository.save(inquiry); - - inquiryAttachmentService.updateAttachment(request.attachments(), inquiry); - } } From 3dbbf2147e771e30ac96adae39fd07eccb94852b Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sun, 10 Aug 2025 21:43:04 +0900 Subject: [PATCH 12/16] feat: initialize DiscordNotifier with webhook URL and prevent notifications if not configured --- .../mosuserver/infra/notify/DiscordNotifier.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java b/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java index 34298cef..e4019cd0 100644 --- a/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java +++ b/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java @@ -1,5 +1,6 @@ package life.mosu.mosuserver.infra.notify; +import jakarta.annotation.PostConstruct; import java.util.Map; import java.util.Objects; import life.mosu.mosuserver.infra.notify.dto.discord.DiscordExceptionNotifyEventRequest; @@ -22,11 +23,16 @@ public class DiscordNotifier implements NotifyClientAdapter Date: Sun, 10 Aug 2025 21:43:58 +0900 Subject: [PATCH 13/16] feat: update DiscordNotifier to use blank check for webhook URL configuration --- .../life/mosu/mosuserver/infra/notify/DiscordNotifier.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java b/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java index e4019cd0..0cc943a0 100644 --- a/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java +++ b/src/main/java/life/mosu/mosuserver/infra/notify/DiscordNotifier.java @@ -2,7 +2,6 @@ import jakarta.annotation.PostConstruct; import java.util.Map; -import java.util.Objects; import life.mosu.mosuserver.infra.notify.dto.discord.DiscordExceptionNotifyEventRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,7 +21,7 @@ public class DiscordNotifier implements NotifyClientAdapter Date: Sun, 10 Aug 2025 21:44:25 +0900 Subject: [PATCH 14/16] feat: increase title size limit in InquiryUpdateRequest to 300 characters --- .../presentation/inquiry/dto/InquiryUpdateRequest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryUpdateRequest.java b/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryUpdateRequest.java index 0e0a26cb..4d83c432 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryUpdateRequest.java +++ b/src/main/java/life/mosu/mosuserver/presentation/inquiry/dto/InquiryUpdateRequest.java @@ -7,7 +7,7 @@ import life.mosu.mosuserver.presentation.common.FileRequest; public record InquiryUpdateRequest( - @Size(max = 100, message = "제목은 최대 300자까지 입력 가능합니다.") + @Size(max = 300, message = "제목은 최대 300자까지 입력 가능합니다.") @Schema(description = "문의 제목", example = "서비스 이용 관련 질문입니다.") @NotNull String title, @@ -15,4 +15,6 @@ public record InquiryUpdateRequest( @Schema(description = "문의 내용", example = "포인트는 어떻게 사용하나요?") @NotNull String content, List attachments -) { } +) { + +} From 08a58cd9f06518c049a9937eca3afc162ef773e0 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sun, 10 Aug 2025 21:45:23 +0900 Subject: [PATCH 15/16] feat: update logging configuration to specify log file path in application YAML files --- src/main/resources/application-prod.yml | 3 +-- src/main/resources/application-test.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index b0021cd1..99c3af57 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,7 +1,6 @@ logging: file: - path: ./logs - name: app.log + name: ./logs/app.log level: root: INFO diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 7dbd9893..5ef36ffc 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -1,7 +1,6 @@ logging: file: - path: ./logs - name: app.log + name: ./logs/app.log level: root: info From 867e96d54ea5f50201c1f2e60bb17828de42fd16 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Sun, 10 Aug 2025 21:52:49 +0900 Subject: [PATCH 16/16] feat: use sudo for Docker build and push commands in self-deploy YAML --- .github/workflows/self-depoly.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/self-depoly.yaml b/.github/workflows/self-depoly.yaml index f077bbdc..56beca69 100644 --- a/.github/workflows/self-depoly.yaml +++ b/.github/workflows/self-depoly.yaml @@ -44,10 +44,10 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: Build Docker image - run: docker build -t kangtaehyun1107/mosu-server:${{ github.sha }} . + run: sudo docker build -t kangtaehyun1107/mosu-server:${{ github.sha }} . working-directory: - name: Push Docker image - run: docker push kangtaehyun1107/mosu-server:${{ github.sha }} + run: sudo docker push kangtaehyun1107/mosu-server:${{ github.sha }} - name: Deploy via SSH run: |