Skip to content

fix: 게시글 중복 생성 방지#649

Merged
JAEHEE25 merged 3 commits intosolid-connection:developfrom
JAEHEE25:fix/640-post-duplication
Feb 12, 2026
Merged

fix: 게시글 중복 생성 방지#649
JAEHEE25 merged 3 commits intosolid-connection:developfrom
JAEHEE25:fix/640-post-duplication

Conversation

@JAEHEE25
Copy link
Contributor

@JAEHEE25 JAEHEE25 commented Feb 9, 2026

관련 이슈

작업 내용

1. redis 패키지 분리

  • redis 관련 코드를 redis 패키지로 분리
  • post에서 redis를 사용하는 코드를 PostRedisManager로 분리

2. 게시글 생성 중복 요청 문제 해결

  • 멱등성 보장을 위해 redis 락 적용
  • SETNX + TTL 기반으로 동일한 사용자의 요청은 최초 요청만 허용하고 이후 요청은 차단
  • TTL(5초) 만료 후 재요청 가능

포스트맨으로 iteration 3, delay 0으로 테스트했을 때

  • 기존 코드 -> 3개 요청 모두 성공
image

  • 작업 후 코드 -> 최초 요청만 성공
image

특이 사항

리뷰 요구사항 (선택)

- Redis 패키지 및 로직 정리
- 게시글 중복 요청 방지 Redis 로직 추가
@JAEHEE25 JAEHEE25 self-assigned this Feb 9, 2026
@JAEHEE25 JAEHEE25 added the 버그 Something isn't working label Feb 9, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Walkthrough

  1. RedisConstants가 com.example.solidconnection.redis로 이동하고, POST_CREATE_PREFIX 및 VALIDATE_POST_CREATE_TTL 상수가 추가되었습니다.
  2. New: PostRedisManager 컴포넌트가 추가되어 포스트 조회수 키 생성, 조회수 획득·삭제, 생성 중복 검증 등 Redis 관련 로직을 캡슐화합니다.
  3. PostCommandService, PostQueryService, UpdateViewCountService 등에서 RedisUtils/RedisService 사용을 제거하고 PostRedisManager로 대체했으며, 게시글 생성 시 중복 생성 방지 검사(isPostCreationAllowed)와 조회수 첫 접근 증분 로직이 삽입되었습니다.
  4. RedisService.isPresent 메서드 시그니처가 TTL 인자를 받도록 변경되었습니다.
  5. 동시성 관련 테스트들이 추가·수정되어 포스트 생성 중복/조회수 동시성 시나리오를 검증합니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • wibaek
  • Hexeong
  • lsy1307
  • sukangpunch
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경 사항인 게시글 중복 생성 방지를 명확하고 간결하게 설명합니다.
Description check ✅ Passed PR 설명이 필수 섹션을 포함하고 있으며, 작업 내용과 테스트 결과를 충분히 설명하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 변경 사항이 #640 이슈의 요구사항인 게시글 중복 생성 방지를 충족합니다.
Out of Scope Changes check ✅ Passed Redis 패키지 분리와 PostRedisManager 생성은 게시글 중복 생성 방지 기능을 구현하기 위한 필요한 리팩토링입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java (1)

46-68: ⚠️ Potential issue | 🟠 Major

중복 방지 락이 유효성 검증보다 먼저 설정되어, 검증 실패 시 사용자가 불필요하게 잠길 수 있습니다.

현재 흐름:

  1. 사용자 조회 ✅
  2. Redis 락 획득 (Line 52) ← 여기서 락 설정
  3. 카테고리 유효성 검증 (Line 57)
  4. 이미지 개수 검증 (Line 58)
  5. DB 저장 (Line 66)

만약 3번이나 4번에서 예외가 발생하면, 락은 이미 설정된 상태이므로 사용자가 입력을 수정해 재시도하더라도 TTL(5초)이 만료될 때까지 DUPLICATE_POST_CREATE_REQUEST 오류를 받게 됩니다.

락 획득 시점을 유효성 검증 이후, DB 저장 직전으로 이동시키는 것을 권장합니다.

🔧 락 획득 시점 이동 제안
     public PostCreateResponse createPost(long siteUserId, PostCreateRequest postCreateRequest,
                                          List<MultipartFile> imageFile) {
         SiteUser siteUser = siteUserRepository.findById(siteUserId)
                 .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
 
-        // 중복 생성 방지
-        if (!postRedisManager.isPostCreationAllowed(siteUserId)) {
-            throw new CustomException(DUPLICATE_POST_CREATE_REQUEST);
-        }
-
         // 유효성 검증
         validatePostCategory(postCreateRequest.postCategory());
         validateFileSize(imageFile);
 
+        // 중복 생성 방지
+        if (!postRedisManager.isPostCreationAllowed(siteUserId)) {
+            throw new CustomException(DUPLICATE_POST_CREATE_REQUEST);
+        }
+
         // 객체 생성
         Board board = boardRepository.getByCode(postCreateRequest.boardCode());
         Post post = postCreateRequest.toEntity(siteUser, board);
🤖 Fix all issues with AI agents
In
`@src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java`:
- Around line 23-28: Remove the unnecessary DB fetch and add null-safety: in
updateViewCount, drop the unused postRepository.getById(postId) call and instead
call postRedisManager.getAndDeleteViewCount(key) into a Long viewCount variable,
then guard against null (e.g., if viewCount == null || viewCount == 0 return; or
treat null as 0) before calling postRepository.increaseViewCount(postId,
viewCount); ensure you still obtain postId via
postRedisManager.getPostIdFromPostViewCountRedisKey(key).

In `@src/main/java/com/example/solidconnection/redis/RedisService.java`:
- Around line 3-4: Remove the unused static import VALIDATE_VIEW_COUNT_TTL from
RedisService.java; keep the existing static import for VIEW_COUNT_TTL (used as
VIEW_COUNT_TTL.getValue()) and ensure no other references to
VALIDATE_VIEW_COUNT_TTL remain in the class (remove any accidental leftover
usages if present).
🧹 Nitpick comments (4)
src/main/java/com/example/solidconnection/redis/RedisService.java (1)

40-43: isPresent 메서드 네이밍이 실제 동작과 반대입니다.

setIfAbsent는 키가 없을 때 값을 설정하고 true를 반환합니다. 즉, isPresenttrue를 반환하는 경우는 키가 존재하지 않았던 상황입니다. 메서드 이름이 실제 의미와 반대여서 호출부에서 혼동을 일으킬 수 있습니다.

  1. tryAcquire 또는 setIfAbsent와 같이 실제 동작을 반영하는 이름으로 변경하면 가독성이 크게 개선됩니다.
  2. ttl 파라미터를 String 대신 long으로 받으면 호출부마다 파싱할 필요가 없어집니다.
♻️ 네이밍 및 파라미터 타입 개선 제안
-    public boolean isPresent(String key, String ttl) {
-        return Boolean.TRUE.equals(redisTemplate.opsForValue()
-                                           .setIfAbsent(key, "1", Long.parseLong(ttl), TimeUnit.SECONDS));
+    public boolean tryAcquire(String key, long ttlSeconds) {
+        return Boolean.TRUE.equals(redisTemplate.opsForValue()
+                                           .setIfAbsent(key, "1", ttlSeconds, TimeUnit.SECONDS));
     }
src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java (1)

265-270: Redis 락 시나리오에 대한 단위 테스트 추가를 추천합니다.

현재 테스트 구조를 보면 다음과 같이 정리됩니다.

  1. 동시성 테스트의 범위
    PostCreateConcurrencyTest는 여러 스레드가 동시에 접근할 때의 동작만 검증하고 있습니다.

  2. 단위 테스트 레벨의 갭
    PostCommandServiceTest의 게시글 생성 테스트들(게시글을_성공적으로_생성한다 등)은 정상 경로만 다루고 있으며, Redis 락이 이미 설정된 상태에서 DUPLICATE_POST_CREATE_REQUEST 예외가 발생하는 시나리오는 없습니다.

  3. 제안 사항
    단위 테스트에서 PostRedisManagerisPostCreationAllowed() 메서드가 false를 반환하는 상황을 직접 시뮬레이션하여, CustomException이 올바르게 던져지는지 검증하는 테스트를 추가하면 좋겠습니다. 이를 통해 비즈니스 로직의 안정성을 더욱 높일 수 있습니다.

src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java (1)

119-146: 기존 코드 관련 참고사항: CountDownLatch 사용에 잠재적 문제가 있습니다.

이번 PR 변경 범위는 아니지만, 참고로 알려드립니다. doneSignalTHREAD_NUMS(1000)으로 초기화되었는데, 첫 번째 루프(Line 122-133)에서 1000개 태스크가 countDown()을 모두 호출한 뒤, 두 번째 루프(Line 135-146)의 countDown()은 이미 0인 latch에 대해 동작하지 않습니다. 따라서 Line 148의 await()는 두 번째 배치 완료를 보장하지 않습니다.

현재는 Line 155의 Thread.sleep이 충분한 버퍼를 제공하여 동작하지만, CountDownLatch2 * THREAD_NUMS로 초기화하는 것이 더 정확합니다.

src/main/java/com/example/solidconnection/community/post/service/PostRedisManager.java (1)

19-21: getPostIdFromPostViewCountRedisKey: 잘못된 키 입력 시 예외 처리 부재

key.substring(...)Long.parseLong를 바로 호출하고 있어, prefix가 일치하지 않는 키가 들어오면 NumberFormatException이 발생합니다. 현재 호출부(UpdateViewCountScheduler)에서 패턴 매칭된 키만 전달하므로 당장 문제는 없지만, 방어적 코딩 관점에서 참고해 주세요.

Comment on lines 23 to 28
public void updateViewCount(String key) {
Long postId = redisUtils.getPostIdFromPostViewCountRedisKey(key);
Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key);
Post post = postRepository.getById(postId);
postRepository.increaseViewCount(postId, redisService.getAndDelete(key));
Long viewCount = postRedisManager.getAndDeleteViewCount(key);
postRepository.increaseViewCount(postId, viewCount);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the UpdateViewCountService file
find . -name "UpdateViewCountService.java" -type f

Repository: solid-connection/solid-connect-server

Length of output: 175


🏁 Script executed:

# If found, read the full file with line numbers
cat -n src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java 2>/dev/null || echo "File not found, searching for it..."

Repository: solid-connection/solid-connect-server

Length of output: 1331


🏁 Script executed:

# Search for PostRedisManager to understand getAndDeleteViewCount
rg -n 'class PostRedisManager' -A 50 --type=java

Repository: solid-connection/solid-connect-server

Length of output: 5607


🏁 Script executed:

# Search for getAndDeleteViewCount implementation
rg -n 'getAndDeleteViewCount' --type=java -B 3 -A 10

Repository: solid-connection/solid-connect-server

Length of output: 2848


🏁 Script executed:

# Search for increaseViewCount implementation
rg -n 'increaseViewCount' --type=java -B 3 -A 10

Repository: solid-connection/solid-connect-server

Length of output: 13808


불필요한 DB 조회 제거 및 null 안전성 처리가 필요합니다.

아래 두 가지 개선 사항을 적용하세요:

  1. 미사용 엔티티 조회 제거 (라인 25): Post post = postRepository.getById(postId);로 조회한 엔티티가 이후에 사용되지 않습니다. 불필요한 DB 쿼리이므로 제거하세요.

  2. Null 안전성 처리 (라인 26–27): getAndDeleteViewCount(key)는 Redis 키가 만료되거나 존재하지 않을 경우 null을 반환할 수 있습니다. 이 값이 null일 때 increaseViewCount()에 전달되면 SQL 쿼리가 예상치 못한 동작을 할 수 있으므로, null 체크를 추가하세요.

수정 제안
     `@Transactional`
     `@Async`
     public void updateViewCount(String key) {
         Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key);
-        Post post = postRepository.getById(postId);
         Long viewCount = postRedisManager.getAndDeleteViewCount(key);
-        postRepository.increaseViewCount(postId, viewCount);
+        if (viewCount != null && viewCount > 0) {
+            postRepository.increaseViewCount(postId, viewCount);
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void updateViewCount(String key) {
Long postId = redisUtils.getPostIdFromPostViewCountRedisKey(key);
Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key);
Post post = postRepository.getById(postId);
postRepository.increaseViewCount(postId, redisService.getAndDelete(key));
Long viewCount = postRedisManager.getAndDeleteViewCount(key);
postRepository.increaseViewCount(postId, viewCount);
}
public void updateViewCount(String key) {
Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key);
Long viewCount = postRedisManager.getAndDeleteViewCount(key);
if (viewCount != null && viewCount > 0) {
postRepository.increaseViewCount(postId, viewCount);
}
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java`
around lines 23 - 28, Remove the unnecessary DB fetch and add null-safety: in
updateViewCount, drop the unused postRepository.getById(postId) call and instead
call postRedisManager.getAndDeleteViewCount(key) into a Long viewCount variable,
then guard against null (e.g., if viewCount == null || viewCount == 0 return; or
treat null as 0) before calling postRepository.increaseViewCount(postId,
viewCount); ensure you still obtain postId via
postRedisManager.getPostIdFromPostViewCountRedisKey(key).

Copy link
Contributor

@Gyuhyeok99 Gyuhyeok99 left a comment

Choose a reason for hiding this comment

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

🔍 PR #649 코드 리뷰

보안, 성능, 테스트 3가지 관점에서 리뷰했습니다.


🔴 수정 필요

1. [보안] 락 획득 시점 문제

PostCommandService.createPost()에서 유효성 검증(카테고리, 이미지) 전에 Redis 락을 걸고 있습니다. 검증 실패해도 5초간 재요청이 차단됩니다.

권장: 유효성 검증을 먼저 수행한 후 락을 획득하거나, 검증 실패 시 Redis 키를 삭제하는 rollback 로직 추가.

2. [보안] Redis 장애 시 fallback 부재

Redis 다운 시 RedisConnectionFailureException이 전파되어 게시글 생성 자체가 불가능해집니다. 중복방지는 부가기능인데 핵심기능을 막는 것은 가용성 문제입니다.

권장: try-catch로 감싸서 Redis 장애 시 락 없이 생성 허용 (graceful degradation).

public boolean isPostCreationAllowed(Long siteUserId) {
    try {
        String key = getPostCreateRedisKey(siteUserId);
        return redisService.isPresent(key, VALIDATE_POST_CREATE_TTL.getValue());
    } catch (Exception e) {
        log.warn("Redis 장애로 중복 생성 방지 비활성화", e);
        return true;
    }
}

3. [성능] RedisService.getAndDelete() NPE 위험

key가 존재하지 않으면 getAndDelete()null을 반환하고, Long.valueOf(null)NumberFormatException 발생. 스케줄러에서 호출되므로 타이밍 이슈로 발생 가능합니다.

권장: null 체크 추가.

public Long getAndDelete(String key) {
    String value = redisTemplate.opsForValue().getAndDelete(key);
    return value != null ? Long.valueOf(value) : null;
}

4. [성능] UpdateViewCountService 불필요한 DB 조회

postRepository.getById(postId) 결과를 사용하지 않습니다. increaseViewCount()는 JPQL UPDATE로 postId만 필요합니다. key 수만큼 불필요한 SELECT가 매 스케줄링마다 발생합니다.

5. [테스트] PostCommandServiceTest에 중복 생성 테스트 없음

핵심 기능인 DUPLICATE_POST_CREATE_REQUEST 예외 발생 단위 테스트가 누락되어 있습니다.

추가 필요한 테스트:

  • 동일 사용자 5초 내 중복 createPost() 호출 시 예외 발생
  • 게시글 생성 실패(유효성 검증 실패) 후 Redis 키 잔존으로 인한 5초 차단 시나리오

🟡 개선 권장

6. [성능] KEYS 명령 사용

RedisUtils.getKeysOrderByExpiration()에서 O(N) 블로킹 명령인 KEYS를 사용합니다. 게시글 수가 많아지면 Redis 전체 성능에 영향을 줄 수 있으므로 SCAN 명령 기반으로 변경을 권장합니다.

7. [테스트] PostViewCountConcurrencyTest CountDownLatch 버그

조회수_조작_문제를_해결한다 테스트에서 CountDownLatch가 첫 번째 루프에서 이미 0이 되어, 두 번째 루프의 완료를 기다리지 않습니다. flaky test가 될 수 있습니다.

8. [보안] isPresent 메서드 네이밍

isPresent는 "이미 존재하는가"를 의미하지만, 실제로는 setIfAbsent(키가 없을 때 true)를 반환합니다. 유지보수 시 혼란을 줄 수 있으므로 네이밍 개선을 권장합니다.


✅ 양호한 부분

  • SETNX + TTL 원자성 보장 (단일 Redis 명령)
  • Lua 스크립트 기반 조회수 증가 (동시성 안전)
  • Redis 키 패턴 보안성 (서버측 인증된 userId 사용)
  • 에러 메시지에 내부 구현 미노출

🤖 Generated with Claude Code Agent Teams (보안/성능/테스트 3명 병렬 리뷰)

Copy link
Member

@whqtker whqtker left a comment

Choose a reason for hiding this comment

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

고생하셨습니다 ~~!


@Component
@RequiredArgsConstructor
public class PostRedisManager {
Copy link
Member

Choose a reason for hiding this comment

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

흩어진 Post 관련 레디스 메서드 한 곳으로 통합하는 거 좋은 거 같아요 !

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java`:
- Around line 55-58: The Redis lock acquired by
PostRedisManager.isPostCreationAllowed(siteUserId) is not released on failures;
wrap the post creation flow in PostCommandService.create* (where
isPostCreationAllowed is called) with try/catch/finally and call a new
PostRedisManager.releasePostCreationLock(siteUserId) in the failure path (or
finally when creation did not succeed) so the SETNX lock is deleted on
exceptions; add releasePostCreationLock in PostRedisManager that deletes the key
(e.g., getPostCreateRedisKey(siteUserId) -> redisService.deleteKey(key)) and
invoke it from the catch/finally in the service when DB/S3/upload or any
subsequent step fails.

Comment on lines +55 to +58
// 중복 생성 방지
if (!postRedisManager.isPostCreationAllowed(siteUserId)) {
throw new CustomException(DUPLICATE_POST_CREATE_REQUEST);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "PostRedisManager.java" -t f --exec cat {}

Repository: solid-connection/solid-connect-server

Length of output: 2330


🏁 Script executed:

fd "PostCommandService.java" -t f --exec cat {}

Repository: solid-connection/solid-connect-server

Length of output: 6411


🏁 Script executed:

fd "RedisService.java" -t f --exec cat {}

Repository: solid-connection/solid-connect-server

Length of output: 1878


🏁 Script executed:

fd "RedisConstants.java" -t f --exec cat {}

Repository: solid-connection/solid-connect-server

Length of output: 789


생성 실패 시 Redis 락이 해제되지 않는 문제를 수정하세요.

  1. 문제의 원인

    • isPostCreationAllowed()는 Redis의 SETNX(setIfAbsent) 명령으로 5초 TTL의 락을 설정합니다.
    • 락이 성공적으로 획득되면 메서드가 true를 반환하고, 이후 Board 조회·Post 저장·S3 이미지 업로드가 진행됩니다.
    • 이 과정 중 예외가 발생하면 트랜잭션은 롤백되지만, Redis 락은 그대로 남아 5초의 TTL이 만료될 때까지 유지됩니다.
  2. 사용자 영향

    • 사용자가 게시글 생성 중 S3 업로드 실패, DB 저장 실패 등의 예외를 만나면, 실제로는 게시글이 생성되지 않습니다.
    • 그러나 Redis 락이 여전히 활성화되어 있어서, 사용자가 즉시 재시도할 때 "게시글이 이미 생성 중입니다" 400 에러를 받게 됩니다.
    • 실제 게시글이 없는데도 최대 5초간 재시도가 차단되므로 사용 경험이 저하됩니다.
  3. 수정 방향

    • 생성 실패 시 락을 명시적으로 해제해야 합니다.
    • try-catch 블록으로 감싸거나, PostRedisManager에 락 해제 메서드를 추가하는 방식을 권장합니다.
🛠️ 제안하는 구현 예시
         // 중복 생성 방지
         if (!postRedisManager.isPostCreationAllowed(siteUserId)) {
             throw new CustomException(DUPLICATE_POST_CREATE_REQUEST);
         }

-        // 객체 생성
-        Board board = boardRepository.getByCode(postCreateRequest.boardCode());
-        Post post = postCreateRequest.toEntity(siteUser, board);
-
-        // 이미지 처리
-        savePostImages(imageFile, post);
-        Post createdPost = postRepository.save(post);
-
-        return PostCreateResponse.from(createdPost);
+        try {
+            // 객체 생성
+            Board board = boardRepository.getByCode(postCreateRequest.boardCode());
+            Post post = postCreateRequest.toEntity(siteUser, board);
+
+            // 이미지 처리
+            savePostImages(imageFile, post);
+            Post createdPost = postRepository.save(post);
+
+            return PostCreateResponse.from(createdPost);
+        } catch (Exception e) {
+            postRedisManager.releasePostCreationLock(siteUserId);
+            throw e;
+        }

PostRedisManager에 다음 메서드를 추가하세요:

public void releasePostCreationLock(long siteUserId) {
    String key = getPostCreateRedisKey(siteUserId);
    redisService.deleteKey(key);
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java`
around lines 55 - 58, The Redis lock acquired by
PostRedisManager.isPostCreationAllowed(siteUserId) is not released on failures;
wrap the post creation flow in PostCommandService.create* (where
isPostCreationAllowed is called) with try/catch/finally and call a new
PostRedisManager.releasePostCreationLock(siteUserId) in the failure path (or
finally when creation did not succeed) so the SETNX lock is deleted on
exceptions; add releasePostCreationLock in PostRedisManager that deletes the key
(e.g., getPostCreateRedisKey(siteUserId) -> redisService.deleteKey(key)) and
invoke it from the catch/finally in the service when DB/S3/upload or any
subsequent step fails.

@JAEHEE25 JAEHEE25 merged commit 61fad78 into solid-connection:develop Feb 12, 2026
2 checks passed
whqtker added a commit that referenced this pull request Feb 19, 2026
* feat: 파견 대학 테이블명 변경 및 협정 대학 테이블 추가하는 DDL 작성 (#620)

* feat: 파견 대학 테이블명 변경 및 협정 대학 테이블 추가하는 DDL 작성

* refactor: 테이블명 변경 및 추가에 따른 엔티티 생성

- 또한 목데이터 생성 로직 수정

* test: 테스트 코드에서 University -> HostUniversity로 변경

* chore: 중복 인덱스 생성 방지를 위해 인덱스 생성 제거

- FK 제약조건을 추가할 때 인덱스가 없다면 알아서 추가하기 때문

* chore: home_university 테이블에 created/updated_at 추가

* refactor: 잘못 설정되었던 테이블 간 연관 관계 재설정 (#622)

* refactor: home_university와 university_info_for_apply가 FK 관계를 가지도록

* chore: FK 변경에 따른 목데이터 수정

* test: 테스트 픽스터 수정

* refactor: 대학 검색 응답 수정 (#624)

* refactor: home_university와 university_info_for_apply가 FK 관계를 가지도록

* chore: FK 변경에 따른 목데이터 수정

* refactor: 필터 검색 엔드포인트 삭제

* refactor: 필터 검색 관련 서비스 로직 삭제

* refactor: 필터 검색 관련 레포지토리 메서드 삭제

* refactor: 필터 검색 관련 DTO 삭제

* test: 필터 검색 관련 테스트 코드 삭제

* refactor: 지원 대학 관련 응답에 협정 대학 이름 추가

* test: 지원 대학 응답 수정에 따른 테스트 수정

* refactor: 간접 참조 대신 연관관계 추가

- N+1 방지를 위해 fetch join도 추가

* test: 간접 참조 방식에서 연관 관계 설정으로 따른 테스트 코드 수정

* chore: 목데이터에서 지원 대학 테이블에 협정 대학 ID를 설정하도록

* test: home university fixture 추가

* refactor: home university에 대한 fetch join 추가

* refactor: s3 버전 업그레이드 및 로직 수정 (#608)

* refactor: s3 sdk 버전 업그레이드
- 의존성 수정
- 버전 업그레이드에 따른 코드 수정

* refactor: 이미지 이외의 파일 관리를 위해 ImgType 의미 명확하도록 수정
- ImgType에서 UploadType으로 변경
- 해당되는 파일 모두 수정

* refactor: s3 테스트 코드 추가

* fix: s3 access-key, secret-key 최신화, 버킷 명칭 올바르게 수정

* fix: ChatService Test 변경점 반영, S3ServiceTest 단위 테스트로 변경
- images->files로 디렉토리 경로 수정

* fix: 이중 비동기 실행문제 해결
- @async에 전적으로 위임

* refactor: S3Service error 메시지 NPE 가능성 제거

* refactor: 수정사항 반영
- UploadType -> UploadPath로 명칭변경
- 컨벤션 수정(미사용 변수 삭제, 들여쓰기, 명칭변경)

* fix: 테스트 코드 오류 수정
- 내부 로직에서 사용하는 fileUploadService 정의

* refactor: 수정사항 반영
- 파일 확장자 상수화
- 확장자 확인로직, 채팅이면 모든 파일 허용, 이미지 확인까지 모두 enum에서 관리
- MultipartFile이 비동기 과정에서 유실되지 않도록 byte로 변환해서 전달
- UrlPrefixResponse PascalCase로 변경

* refactor: 컨벤션 수정
- 사용하지 않는 import문 삭제

* refactor: 리프레시 토큰 만료시 쿠키 삭제 (#628)

* refactor: 리프레시 토큰 만료시 쿠키 삭제

* refactor: 인증 전용 예외 생성

* refactor: 멘토링 조회 응답에 mentoringId 필드 추가 (#638)

* feat: WebSocket 로깅 인터셉터 작성 (#635)

* feat: WebSocket 로깅 인터셉터 작성

* refactor: Principal 명시적 형 변환 대신 null 체크하여 형 변환

* feat: 어드민에서 파견 대학을 관리하도록 (#633)

* feat: 파견 대학 CRUD 관련 ErrorCode 추가

- HOST_UNIVERSITY_HAS_REFERENCES : 파견 대학 삭제 시 해당 대학을 참조하는 UnivApplyInfo가 존재하는 경우

* feat: 파견 대학 관련 정보를 업데이트하는 도메인 메서드 작성

* feat: 조회 관련 Repository 메서드 구현

* feat: 파견 대학 검색 관련 QueryDSL로 구현

* feat: 어드민 파견 대학 CRUD 관련 DTO 작성

* feat: country 조회 관련 로직 추가 및 ErrorCode 추가

* feat: 어드민 파견 대학 CRUD 관련 서비스 로직 작성

* feat: 어드민 파견 대학 관련 컨트롤러 작성

* test: 어드민 파견 대학 관리 관련 테스트 작성

* refactor: 엔드포인트의 path variable 이름 변경

- id -> host-university-id

* refactor: PageResponse 응답 객체를 사용하도록

* test: 응답 변경에 따른 테스트 코드 수정

* fix: host_university 테이블의 korean_name 필드에 unique key 추가 (#645)

* fix: host_university 테이블의 korean_name 필드에 unique key 쿠가

* test: test용 hostUniversityRepository 생성

* test: 고유한 korean_name을 가진 host university 객체를 사용하도록

* fix: 멘토 지원서 승인 시 유저 Role 을 Mentor로 승격 (#639)

* fix: 멘토 지원서 승인 시 유저 Role 을 Mentor로 승격

* fix: 멘토 지원서 승인 시 멘토 생성

* fix: 멘토의 introduction, passTip null 허용하도록 수정

- not null 인 필드에 빈문자열로 값을 채우는 것 보다, null 허용이 더 의미
  있다 판단하여 null 을 허용하도록 하였습니다.

* fix: 사용하지 않는  멘토 생성 api 제거

- 멘토 생성의 주체가 어드민으로 변경되어 Mentor 도메인의 Mentor 생성 api 를 제거

* feat: 멘토 지원서 승인 예외처리 추가

- 중복 멘토 생성 예외 처리 및 테스트 추가

* refactor: Mentor 생성 시 null 전달 제거

* refactor: 멘토 지원서 승낙 시, 검증 후 승격 및 멘토 생성

* chore: 스크립트 버전 수정 (#651)

* chore: 스크립트 버전 수정

* test: korean_name 컬럼 UK 관련 테스트 코드 수정

* feat: test skill 추가 (#647)

* feat: serena MCP 추가

* feat: test skill 추가

* feat: hook 추가

- 응답 대기시 알람발송
- 컨벤션 어겼을 때 훅 작동

* feat: 안쓰는 파일 제거

* fix: 게시글 중복 생성 방지 (#649)

* fix: 게시글 중복 생성 방지

- Redis 패키지 및 로직 정리

* fix: 게시글 중복 생성 방지

- 게시글 중복 요청 방지 Redis 로직 추가

* refactor: 게시글 중복 생성 방지

* chore: testcontainer 버전 업 (#659)

* chore: windows에서도 hook이 동작하도록 (#655)

* refactor: 오래된 이미지 삭제 후 이미지 pull하도록 변경 (#653)

refactor: 오래된 이미지 삭제 후 이미지 pull하도록 변경 (#653)

- 추가로 이미지는 5개 -> 2개 보관하도록 변경

* refactor: 멘토 도메인 응답의 사용자 id를 siteUserId로 통일 (#665)

* refactor: 멘토 관련 id응답은 모두 site-user-id가 되도록 수정

* test: 멘토 관련 테스트 코드 수정

* refactor: 채팅 도메인 응답의 사용자 관련 id를 siteUserId로 통일 (#666)

* refactor: 채팅 관련 응답에서 사용자 관련 Id를 siteUserId로 통일

* refactor: siteUserId를 포함하도록 서비스 코드 수정

* test: 사용자 id로 응답 통일 관련 테스트 수정

* feat: 전체 뉴스를 조회하는 API 구현 (#674)

* feat: 전체 news 조회 API 구현

- 기존 API에 author-id를 선택적으로 받도록

* test: 전체 news 조회 관련 테스트 코드 작성

* refactor: 날짜 오름차순으로 news 조회하는 JPA 메서드 추가

* refactor: 뉴스 조회 API를 하나로 통합

- 서비스 계층에서 siteUserId == null을 기준으로 분기하도록

* refactor: 컨트롤러 계층에서 분기문 제거

- 분기를 서비스 계층에게 위임했음

* test: 뉴스 조회 관련 테스트 코드 수정

* chore: 누락된 제약 조건을 추가하는 스크립트 작성 (#676)

---------

Co-authored-by: Yeon <84384499+lsy1307@users.noreply.github.com>
Co-authored-by: 황규혁 <126947828+Gyuhyeok99@users.noreply.github.com>
Co-authored-by: hyungjun <115551339+sukangpunch@users.noreply.github.com>
Co-authored-by: 정재희 <y2hjjh@naver.com>
@whqtker whqtker mentioned this pull request Feb 19, 2026
whqtker added a commit that referenced this pull request Mar 8, 2026
* feat: 파견 대학 테이블명 변경 및 협정 대학 테이블 추가하는 DDL 작성 (#620)

* feat: 파견 대학 테이블명 변경 및 협정 대학 테이블 추가하는 DDL 작성

* refactor: 테이블명 변경 및 추가에 따른 엔티티 생성

- 또한 목데이터 생성 로직 수정

* test: 테스트 코드에서 University -> HostUniversity로 변경

* chore: 중복 인덱스 생성 방지를 위해 인덱스 생성 제거

- FK 제약조건을 추가할 때 인덱스가 없다면 알아서 추가하기 때문

* chore: home_university 테이블에 created/updated_at 추가

* refactor: 잘못 설정되었던 테이블 간 연관 관계 재설정 (#622)

* refactor: home_university와 university_info_for_apply가 FK 관계를 가지도록

* chore: FK 변경에 따른 목데이터 수정

* test: 테스트 픽스터 수정

* refactor: 대학 검색 응답 수정 (#624)

* refactor: home_university와 university_info_for_apply가 FK 관계를 가지도록

* chore: FK 변경에 따른 목데이터 수정

* refactor: 필터 검색 엔드포인트 삭제

* refactor: 필터 검색 관련 서비스 로직 삭제

* refactor: 필터 검색 관련 레포지토리 메서드 삭제

* refactor: 필터 검색 관련 DTO 삭제

* test: 필터 검색 관련 테스트 코드 삭제

* refactor: 지원 대학 관련 응답에 협정 대학 이름 추가

* test: 지원 대학 응답 수정에 따른 테스트 수정

* refactor: 간접 참조 대신 연관관계 추가

- N+1 방지를 위해 fetch join도 추가

* test: 간접 참조 방식에서 연관 관계 설정으로 따른 테스트 코드 수정

* chore: 목데이터에서 지원 대학 테이블에 협정 대학 ID를 설정하도록

* test: home university fixture 추가

* refactor: home university에 대한 fetch join 추가

* refactor: s3 버전 업그레이드 및 로직 수정 (#608)

* refactor: s3 sdk 버전 업그레이드
- 의존성 수정
- 버전 업그레이드에 따른 코드 수정

* refactor: 이미지 이외의 파일 관리를 위해 ImgType 의미 명확하도록 수정
- ImgType에서 UploadType으로 변경
- 해당되는 파일 모두 수정

* refactor: s3 테스트 코드 추가

* fix: s3 access-key, secret-key 최신화, 버킷 명칭 올바르게 수정

* fix: ChatService Test 변경점 반영, S3ServiceTest 단위 테스트로 변경
- images->files로 디렉토리 경로 수정

* fix: 이중 비동기 실행문제 해결
- @async에 전적으로 위임

* refactor: S3Service error 메시지 NPE 가능성 제거

* refactor: 수정사항 반영
- UploadType -> UploadPath로 명칭변경
- 컨벤션 수정(미사용 변수 삭제, 들여쓰기, 명칭변경)

* fix: 테스트 코드 오류 수정
- 내부 로직에서 사용하는 fileUploadService 정의

* refactor: 수정사항 반영
- 파일 확장자 상수화
- 확장자 확인로직, 채팅이면 모든 파일 허용, 이미지 확인까지 모두 enum에서 관리
- MultipartFile이 비동기 과정에서 유실되지 않도록 byte로 변환해서 전달
- UrlPrefixResponse PascalCase로 변경

* refactor: 컨벤션 수정
- 사용하지 않는 import문 삭제

* refactor: 리프레시 토큰 만료시 쿠키 삭제 (#628)

* refactor: 리프레시 토큰 만료시 쿠키 삭제

* refactor: 인증 전용 예외 생성

* refactor: 멘토링 조회 응답에 mentoringId 필드 추가 (#638)

* feat: WebSocket 로깅 인터셉터 작성 (#635)

* feat: WebSocket 로깅 인터셉터 작성

* refactor: Principal 명시적 형 변환 대신 null 체크하여 형 변환

* feat: 어드민에서 파견 대학을 관리하도록 (#633)

* feat: 파견 대학 CRUD 관련 ErrorCode 추가

- HOST_UNIVERSITY_HAS_REFERENCES : 파견 대학 삭제 시 해당 대학을 참조하는 UnivApplyInfo가 존재하는 경우

* feat: 파견 대학 관련 정보를 업데이트하는 도메인 메서드 작성

* feat: 조회 관련 Repository 메서드 구현

* feat: 파견 대학 검색 관련 QueryDSL로 구현

* feat: 어드민 파견 대학 CRUD 관련 DTO 작성

* feat: country 조회 관련 로직 추가 및 ErrorCode 추가

* feat: 어드민 파견 대학 CRUD 관련 서비스 로직 작성

* feat: 어드민 파견 대학 관련 컨트롤러 작성

* test: 어드민 파견 대학 관리 관련 테스트 작성

* refactor: 엔드포인트의 path variable 이름 변경

- id -> host-university-id

* refactor: PageResponse 응답 객체를 사용하도록

* test: 응답 변경에 따른 테스트 코드 수정

* fix: host_university 테이블의 korean_name 필드에 unique key 추가 (#645)

* fix: host_university 테이블의 korean_name 필드에 unique key 쿠가

* test: test용 hostUniversityRepository 생성

* test: 고유한 korean_name을 가진 host university 객체를 사용하도록

* fix: 멘토 지원서 승인 시 유저 Role 을 Mentor로 승격 (#639)

* fix: 멘토 지원서 승인 시 유저 Role 을 Mentor로 승격

* fix: 멘토 지원서 승인 시 멘토 생성

* fix: 멘토의 introduction, passTip null 허용하도록 수정

- not null 인 필드에 빈문자열로 값을 채우는 것 보다, null 허용이 더 의미
  있다 판단하여 null 을 허용하도록 하였습니다.

* fix: 사용하지 않는  멘토 생성 api 제거

- 멘토 생성의 주체가 어드민으로 변경되어 Mentor 도메인의 Mentor 생성 api 를 제거

* feat: 멘토 지원서 승인 예외처리 추가

- 중복 멘토 생성 예외 처리 및 테스트 추가

* refactor: Mentor 생성 시 null 전달 제거

* refactor: 멘토 지원서 승낙 시, 검증 후 승격 및 멘토 생성

* chore: 스크립트 버전 수정 (#651)

* chore: 스크립트 버전 수정

* test: korean_name 컬럼 UK 관련 테스트 코드 수정

* feat: test skill 추가 (#647)

* feat: serena MCP 추가

* feat: test skill 추가

* feat: hook 추가

- 응답 대기시 알람발송
- 컨벤션 어겼을 때 훅 작동

* feat: 안쓰는 파일 제거

* fix: 게시글 중복 생성 방지 (#649)

* fix: 게시글 중복 생성 방지

- Redis 패키지 및 로직 정리

* fix: 게시글 중복 생성 방지

- 게시글 중복 요청 방지 Redis 로직 추가

* refactor: 게시글 중복 생성 방지

* chore: testcontainer 버전 업 (#659)

* chore: windows에서도 hook이 동작하도록 (#655)

* refactor: 오래된 이미지 삭제 후 이미지 pull하도록 변경 (#653)

refactor: 오래된 이미지 삭제 후 이미지 pull하도록 변경 (#653)

- 추가로 이미지는 5개 -> 2개 보관하도록 변경

* refactor: 멘토 도메인 응답의 사용자 id를 siteUserId로 통일 (#665)

* refactor: 멘토 관련 id응답은 모두 site-user-id가 되도록 수정

* test: 멘토 관련 테스트 코드 수정

* refactor: 채팅 도메인 응답의 사용자 관련 id를 siteUserId로 통일 (#666)

* refactor: 채팅 관련 응답에서 사용자 관련 Id를 siteUserId로 통일

* refactor: siteUserId를 포함하도록 서비스 코드 수정

* test: 사용자 id로 응답 통일 관련 테스트 수정

* feat: 전체 뉴스를 조회하는 API 구현 (#674)

* feat: 전체 news 조회 API 구현

- 기존 API에 author-id를 선택적으로 받도록

* test: 전체 news 조회 관련 테스트 코드 작성

* refactor: 날짜 오름차순으로 news 조회하는 JPA 메서드 추가

* refactor: 뉴스 조회 API를 하나로 통합

- 서비스 계층에서 siteUserId == null을 기준으로 분기하도록

* refactor: 컨트롤러 계층에서 분기문 제거

- 분기를 서비스 계층에게 위임했음

* test: 뉴스 조회 관련 테스트 코드 수정

* chore: 누락된 제약 조건을 추가하는 스크립트 작성 (#676)

* chore: pr을 리뷰하는 SKILL.md 작성 (#669)

* chore: pr을 리뷰하는 SKILL.md 작성

* chore: vscode extension으로 claude를 사용하는 경우에도 알림이 뜨도록

* feat: Parameter Store 기반 설정 관리 전환 및 loadtest 프로필 추가 (#670)

* feat: Parameter Store 설정 전환

- secret 서브모듈 제거 및 설정 파일 분리

- 프로필별 Parameter Store import와 loadtest 프로필 추가

* feat: Parameter Store 연동 설정 추가

- Spring Cloud AWS Parameter Store 의존성 추가

- config 분리와 profile별 import/loadtest 프로필 설정

* feat: 배포 파이프라인 Parameter Store 대응

- dev/prod workflow에서 submodule checkout 제거

- compose 환경변수에 AWS_REGION 주입

* feat: S3 인증을 IAM Role 체인으로 전환

- 정적 access key/secret key 주입 제거

- DefaultCredentialsProvider 기반 S3Client 구성

* feat: 리뷰 반영 설정 보완

- local 프로필 refresh cookie-domain을 localhost로 수정

- loadtest outbound thread pool에 queue-capacity 추가

* refactor: 환경별 설정 fail-fast 정리

- websocket outbound queue-capacity 누락 보완

- prod/dev/local OAuth redirect-url fallback 제거

- yml placeholder 빈 기본값(:) 제거

- dev CORS admin 도메인 변경 반영

* chore: Parameter Store import를 fail-fast로 전환

- common/local/dev/prod/loadtest aws-parameterstore import에서 optional 제거

* refactor: cloud 설정을 SSM 단일 소스로 정리

- env placeholder(CLOUD_AWS_*) 제거

- cloud.aws.* 값은 /solid-connection/common 경로에서 주입

* refactor: DB/Flyway 설정을 프로퍼티 키 기반으로 정리

- datasource/flyway env placeholder 제거

- prod/dev baseline-on-migrate를 false로 변경

- local 빈 redis 블록 제거

* chore: 공통 변수 설정 보안/컨벤션 정리

- OAuth/JWT/Sentry/News/Loki placeholder 제거

- kakao user-info-url 키를 kebab-case로 통일

- sentry 기본 PII 비활성화 및 prod traces 샘플링 0.1 설정

- dev admin CORS origin 문자열 변경 반영

* fix: dev admin CORS origin trailing slash 제거

* feat: review comments 반영

- loadtest 프로필에 spring.flyway.enabled=false 명시

- test 설정의 kakao user-info 키를 user-info-url로 통일

* refactor: docker-compose.dev.yml에 mysql 컨테이너 추가 (#690)

* refactor: docker-compose.dev.yml에 mysql 컨테이너 추가

* fix: 코드래빗 반영(MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' 추가)

* fix: term 테이블의 is_current 필드에 UK 설정 (#650)

* fix: is_current 필드에 UK 설정

* test: UK 설정에 따른 테스트 코드 불일치성 해결

* chore: 스크립트 버전 수정

* refactor: 미사용 코드 제거

* feat: 어드민 유저 관리 기능 추가 (#686)

* feat: 어드민 유저 관리 기능

- 차단 기능 rebase 및 충돌 해결

* test: 어드민 유저 관리 기능

* feat: 어드민 유저 관리 기능

* feat: 어드민 유저 관리 기능

- University -> HostUniversity 변경

* refactor: 어드민 유저 관리 기능

* chore: 스크립트 버전 수정 (#691)

* chore: stage admin 도메인 추가 (#692)

* refactor: 응답에서 refresh token은 제외하여 전달하도록 (#646)

* refactor: 응답에서 refresh token은 제외하여 전달하도록

* refactor: 미사용 응답 필드 제거

* refactor: 쿠키 설정용, HTTP 응답용 DTO 변경

* test: result 객체 추가에 따른 테스트 코드 수정

* feat: 어드민 국가 crud 추가 (#656)

* feat: 국가 관련 어드민 crud 추가

* test: 국가 관련 어드민 crud 테스트 추가

* refactor: 대학 캐시 무효화 정책 추가 (#660)

* refactor: 대학 캐시 무효화 정책 추가

* test: 대학 캐시 무효화 정책 테스트 추가

* chore: master hotfix를 develop에 sync (#694)

* fix: /applications 중복 활성 지원서 조회 시 500 오류 방지

- 활성 지원서 조회를 최신 1건 기준 메서드로 변경

- 지원서 제출 로직과 테스트 코드를 변경된 조회 메서드명에 맞춰 동기화

* fix: 활성 지원서 중복 문제의 근본 해결 TODO 명시

- 최신 1건 선택 로직이 임시 회피임을 주석으로 명확화

- DB 유일성 제약 도입 후 우회 조회 제거 필요사항을 기록

* hotfix: 대학 조회 캐싱 임시 제거

* hotfix: 대학 조회 캐싱 임시 제거

---------

Co-authored-by: Wibaek Park <34394229+devMuromi@users.noreply.github.com>
Co-authored-by: 이세원 <107756067+leesewon00@users.noreply.github.com>
Co-authored-by: Wibaek Park <34394229+wibaek@users.noreply.github.com>
Co-authored-by: Yeongseo Na <panda0329@naver.com>
Co-authored-by: Wibaek Park <devmuromi@gmail.com>
Co-authored-by: 황규혁 <126947828+Gyuhyeok99@users.noreply.github.com>
Co-authored-by: Gyuhyeok99 <ghkdrbgur13@naver.com>
Co-authored-by: Yeon <84384499+lsy1307@users.noreply.github.com>
Co-authored-by: hyungjun <115551339+sukangpunch@users.noreply.github.com>
Co-authored-by: 정재희 <y2hjjh@naver.com>
Co-authored-by: Yeonri <lsy1307@inha.edu>

---------

Co-authored-by: Yeon <84384499+lsy1307@users.noreply.github.com>
Co-authored-by: 황규혁 <126947828+Gyuhyeok99@users.noreply.github.com>
Co-authored-by: hyungjun <115551339+sukangpunch@users.noreply.github.com>
Co-authored-by: 정재희 <y2hjjh@naver.com>
Co-authored-by: in seong Park <74069492+Hexeong@users.noreply.github.com>
Co-authored-by: Wibaek Park <34394229+devMuromi@users.noreply.github.com>
Co-authored-by: 이세원 <107756067+leesewon00@users.noreply.github.com>
Co-authored-by: Wibaek Park <34394229+wibaek@users.noreply.github.com>
Co-authored-by: Yeongseo Na <panda0329@naver.com>
Co-authored-by: Wibaek Park <devmuromi@gmail.com>
Co-authored-by: Gyuhyeok99 <ghkdrbgur13@naver.com>
Co-authored-by: Yeonri <lsy1307@inha.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

버그 Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: 게시글 중복 생성 방지

3 participants