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 @@ -13,4 +13,6 @@ public interface TagRepository extends JpaRepository<Tag, UUID> {
List<Tag> findByNameIgnoreCaseIn(List<String> names);

java.util.Optional<Tag> findByName(String name);

java.util.Optional<Tag> findByNameIgnoreCase(String name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ public String resolvePreferredAiLevel(UUID userId, String requestedLevel) {
.orElse("JUNIOR");
}

/** 태그명으로 Tag 조회, 없으면 신규 생성 후 반환. */
/** 태그명으로 대소문자 무관하게 Tag 조회, 없으면 신규 생성 후 반환. */
private List<Tag> findOrCreateTags(List<String> names) {
return names.stream()
.map(name -> tagRepository.findByName(name)
.map(name -> tagRepository.findByNameIgnoreCase(name)
.orElseGet(() -> tagRepository.save(Tag.builder().name(name).build())))
.toList();
}
Expand Down
69 changes: 59 additions & 10 deletions src/main/resources/data.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
-- DP-462: 중복 태그 제거 — css→CSS, Github→GitHub 병합 (case 버그로 생성된 중복 row 정리)
DO $$
DECLARE
v_dup_id UUID;
v_keep_id UUID;
BEGIN
-- css → CSS 병합
SELECT id INTO v_dup_id FROM tags WHERE name = 'css';
SELECT id INTO v_keep_id FROM tags WHERE name = 'CSS';
IF v_dup_id IS NOT NULL AND v_keep_id IS NOT NULL THEN
UPDATE content_tags SET tag_id = v_keep_id
WHERE tag_id = v_dup_id
AND NOT EXISTS (SELECT 1 FROM content_tags x WHERE x.content_id = content_tags.content_id AND x.tag_id = v_keep_id);
DELETE FROM content_tags WHERE tag_id = v_dup_id;
UPDATE user_tags SET tag_id = v_keep_id
WHERE tag_id = v_dup_id
AND NOT EXISTS (SELECT 1 FROM user_tags x WHERE x.user_id = user_tags.user_id AND x.tag_id = v_keep_id);
DELETE FROM user_tags WHERE tag_id = v_dup_id;
DELETE FROM tags WHERE id = v_dup_id;
END IF;

-- Github → GitHub 병합
SELECT id INTO v_dup_id FROM tags WHERE name = 'Github';
SELECT id INTO v_keep_id FROM tags WHERE name = 'GitHub';
IF v_dup_id IS NOT NULL AND v_keep_id IS NOT NULL THEN
UPDATE content_tags SET tag_id = v_keep_id
WHERE tag_id = v_dup_id
AND NOT EXISTS (SELECT 1 FROM content_tags x WHERE x.content_id = content_tags.content_id AND x.tag_id = v_keep_id);
DELETE FROM content_tags WHERE tag_id = v_dup_id;
UPDATE user_tags SET tag_id = v_keep_id
WHERE tag_id = v_dup_id
AND NOT EXISTS (SELECT 1 FROM user_tags x WHERE x.user_id = user_tags.user_id AND x.tag_id = v_keep_id);
DELETE FROM user_tags WHERE tag_id = v_dup_id;
DELETE FROM tags WHERE id = v_dup_id;
END IF;
END $$;

-- DP-462: 한국어 태그 → 영어 rename (AI 프롬프트 기준값 일치)
UPDATE tags SET name = 'Algorithm' WHERE name = '알고리즘';
UPDATE tags SET name = 'Data Structure' WHERE name = '자료구조';
UPDATE tags SET name = 'OS' WHERE name = '운영체제';
UPDATE tags SET name = 'Network' WHERE name = '네트워크';
UPDATE tags SET name = 'Design Pattern' WHERE name = '디자인패턴';
UPDATE tags SET name = 'Compiler' WHERE name = '컴파일러';
UPDATE tags SET name = 'Concurrency' WHERE name = '동시성';
UPDATE tags SET name = 'Parallel Programming' WHERE name = '병렬프로그래밍';
UPDATE tags SET name = 'Memory Management' WHERE name = '메모리관리';
UPDATE tags SET name = 'Garbage Collection' WHERE name = '가비지컬렉션';

-- DP-150: 태그 초기 데이터 삽입
-- 서버 시작 시 tags 테이블에 기본 태그들을 삽입한다.
-- 이미 존재하는 태그는 건너뜀 (ON CONFLICT DO NOTHING)
Expand Down Expand Up @@ -64,18 +113,18 @@ INSERT INTO tags (id, name, created_at) VALUES
-- 아키텍처 / 설계
(gen_random_uuid(), 'MSA', NOW()),
(gen_random_uuid(), '시스템설계', NOW()),
(gen_random_uuid(), '디자인패턴', NOW()),
(gen_random_uuid(), 'Design Pattern', NOW()),
(gen_random_uuid(), '클린코드', NOW()),
(gen_random_uuid(), '객체지향', NOW()),
(gen_random_uuid(), '함수형프로그래밍', NOW()),
(gen_random_uuid(), 'DDD', NOW()),
-- CS 기초
(gen_random_uuid(), '네트워크', NOW()),
(gen_random_uuid(), '운영체제', NOW()),
(gen_random_uuid(), '컴파일러', NOW()),
(gen_random_uuid(), 'Network', NOW()),
(gen_random_uuid(), 'OS', NOW()),
(gen_random_uuid(), 'Compiler', NOW()),
(gen_random_uuid(), '데이터베이스이론', NOW()),
(gen_random_uuid(), '컴퓨터구조', NOW()),
(gen_random_uuid(), '병렬프로그래밍', NOW()),
(gen_random_uuid(), 'Parallel Programming', NOW()),
-- AI 모델 / 플랫폼
(gen_random_uuid(), 'Claude', NOW()),
(gen_random_uuid(), 'ChatGPT', NOW()),
Expand Down Expand Up @@ -154,13 +203,13 @@ INSERT INTO tags (id, name, created_at) VALUES
(gen_random_uuid(), 'Hugging Face', NOW()),
(gen_random_uuid(), 'Ollama', NOW()),
-- CS 심화
(gen_random_uuid(), '동시성', NOW()),
(gen_random_uuid(), '메모리관리', NOW()),
(gen_random_uuid(), '가비지컬렉션', NOW()),
(gen_random_uuid(), 'Concurrency', NOW()),
(gen_random_uuid(), 'Memory Management', NOW()),
(gen_random_uuid(), 'Garbage Collection', NOW()),
-- 기타
(gen_random_uuid(), 'Git', NOW()),
(gen_random_uuid(), '알고리즘', NOW()),
(gen_random_uuid(), '자료구조', NOW()),
(gen_random_uuid(), 'Algorithm', NOW()),
(gen_random_uuid(), 'Data Structure', NOW()),
(gen_random_uuid(), '보안', NOW()),
(gen_random_uuid(), '테스트', NOW()),
(gen_random_uuid(), 'AI/ML', NOW()),
Expand Down
35 changes: 32 additions & 3 deletions src/test/java/com/devpick/domain/user/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ void updateProfile_duplicateNickname_throwsException() {
void updateProfile_tags_returnsUpdatedTags() {
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
Tag reactTag = Tag.builder().name("React").build();
given(tagRepository.findByName("React")).willReturn(Optional.of(reactTag));
given(tagRepository.findByNameIgnoreCase("React")).willReturn(Optional.of(reactTag));
UserProfileUpdateRequest request = new UserProfileUpdateRequest(null, null, null, null, List.of("React"));

UserProfileUpdateResponse response = userService.updateProfile(userId, request);
Expand All @@ -260,8 +260,8 @@ void updateProfile_tags_replacedWithoutDuplicate() {
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
Tag reactTag = Tag.builder().name("React").build();
Tag tsTag = Tag.builder().name("TypeScript").build();
given(tagRepository.findByName("React")).willReturn(Optional.of(reactTag));
given(tagRepository.findByName("TypeScript")).willReturn(Optional.of(tsTag));
given(tagRepository.findByNameIgnoreCase("React")).willReturn(Optional.of(reactTag));
given(tagRepository.findByNameIgnoreCase("TypeScript")).willReturn(Optional.of(tsTag));
UserProfileUpdateRequest request = new UserProfileUpdateRequest(null, null, null, null, List.of("React", "TypeScript"));

UserProfileUpdateResponse response = userService.updateProfile(userId, request);
Expand All @@ -270,6 +270,35 @@ void updateProfile_tags_replacedWithoutDuplicate() {
assertThat(response.tags()).doesNotHaveDuplicates();
}

@Test
@DisplayName("updateProfile — 대소문자 다른 태그 입력 시 기존 tag row 재사용, 신규 row 미생성")
void updateProfile_tags_caseInsensitive_reusesExistingTag() {
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
Tag cssTag = Tag.builder().name("CSS").build();
given(tagRepository.findByNameIgnoreCase("css")).willReturn(Optional.of(cssTag));
UserProfileUpdateRequest request = new UserProfileUpdateRequest(null, null, null, null, List.of("css"));

UserProfileUpdateResponse response = userService.updateProfile(userId, request);

assertThat(response.tags()).containsExactly("CSS");
verify(tagRepository, never()).save(any());
}

@Test
@DisplayName("updateProfile — 존재하지 않는 태그는 신규 생성된다")
void updateProfile_tags_unknown_createsNewRow() {
given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.of(user));
Tag newTag = Tag.builder().name("Zig").build();
given(tagRepository.findByNameIgnoreCase("Zig")).willReturn(Optional.empty());
given(tagRepository.save(any(Tag.class))).willReturn(newTag);
UserProfileUpdateRequest request = new UserProfileUpdateRequest(null, null, null, null, List.of("Zig"));

UserProfileUpdateResponse response = userService.updateProfile(userId, request);

assertThat(response.tags()).containsExactly("Zig");
verify(tagRepository).save(any(Tag.class));
}

@Test
@DisplayName("deleteAccount — 소프트 삭제 및 리프레시 토큰 무효화")
void deleteAccount_success() {
Expand Down
Loading