Skip to content

[feat] #79 - 태그 api#85

Merged
laura-jung merged 7 commits into
developfrom
feat/#79-tag
Jul 9, 2026
Merged

[feat] #79 - 태그 api#85
laura-jung merged 7 commits into
developfrom
feat/#79-tag

Conversation

@laura-jung

@laura-jung laura-jung commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

관련 이슈 🛠

작업 내용 요약 ✏️

태그의 생성, 삭제, 조회 api

주요 변경 사항 🛠️

  • Tag 엔티티에 user(LAZY, nullable)와 isDefault를 추가하고 unique(user_id, name)을 걸었습니다. user가 null이면 모든 사용자가 공유하는 기본 태그이고, 생성 API로 만들어지는 태그는 항상 요청자 소유에 isDefault = false입니다.
  • TagService.deleteTag()가 ID 검증 → 조회 → 기본 태그 차단 → 소유자 확인 순으로 검사합니다. 기본 태그는 삭제할 수 없습니다
  • findAccessibleTags()가 생성 API의 중복 검사와 똑같은 조건으로 기본 태그와 내 태그를 함께 가져옵니다.

스크린샷 📷

삭제
스크린샷 2026-07-09 오후 9 43 48
기본 태그 삭제 불가
스크린샷 2026-07-09 오후 9 43 28
생성
스크린샷 2026-07-09 오후 9 43 11
조회
스크린샷 2026-07-09 오후 9 42 56

리뷰 요구사항 📢

db에 미리 기본 태그들 넣어두어야합니다. 마이그레이션 파일로 첨부해두었으니 확인 부탁드립니다.

Summary by CodeRabbit

  • New Features

    • 태그 생성, 목록 조회, 삭제를 지원하는 API가 추가되었습니다.
    • 사용자별 태그와 기본 태그를 함께 다룰 수 있게 되었고, 기본 태그 초기 데이터가 포함되었습니다.
    • 태그 생성/목록 응답에 기본 태그 여부가 표시됩니다.
  • Bug Fixes

    • 태그 이름 중복, 잘못된 태그 ID, 삭제 불가 태그 등 상황별 오류 응답이 개선되었습니다.
    • 태그 입력값 검증과 예외 처리가 일관되게 적용됩니다.

@laura-jung laura-jung requested review from Jy000n and aneykrap July 9, 2026 12:48
@laura-jung laura-jung self-assigned this Jul 9, 2026
@laura-jung laura-jung linked an issue Jul 9, 2026 that may be closed by this pull request
3 tasks
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@laura-jung, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ede07c71-762f-4fac-834c-0b6ef0208b97

📥 Commits

Reviewing files that changed from the base of the PR and between b7c3b1e and 3b4ae0a.

📒 Files selected for processing (1)
  • src/main/java/com/Timo/Timo/domain/tag/dto/request/TagCreateRequest.java

Walkthrough

Tag 엔티티에 사용자 소유(user_id)와 기본 태그(is_default) 개념을 도입하는 DB 마이그레이션과 엔티티 변경이 이루어졌다. TagController, TagService, TagRepository, DTO, 예외/성공 코드 클래스가 새로 추가되어 태그 생성/조회/삭제 REST API를 구현한다.

Changes

태그 소유자/기본값 도입 및 API 구현

Layer / File(s) Summary
DB 마이그레이션 및 Tag 엔티티 확장
db/migration/2026-07-09__tags_add_owner_and_default.sql, src/main/java/.../tag/entity/Tag.java
tags 테이블에 user_id, is_default 컬럼 및 기본 태그 데이터 삽입 마이그레이션을 추가하고, Tag 엔티티에 User ManyToOne 연관, isDefault 필드, name 길이 20→10 변경, ofUser 팩토리 메서드를 도입했다.
리포지토리 접근 제어 쿼리
src/main/java/.../tag/repository/TagRepository.java
사용자 소유 또는 기본 태그에 한정한 존재 여부(existsAccessibleByName) 및 목록 조회(findAccessibleTags) JPQL 메서드를 추가했다.
에러/성공 코드 정의
src/main/java/.../tag/exception/TagErrorCode.java, src/main/java/.../tag/exception/TagSuccessCode.java
TagErrorCode에 INVALID_REQUEST, INVALID_TAG_ID, TAG_DELETE_FORBIDDEN, DUPLICATE_TAG_NAME을 추가했고, TagSuccessCode에 CREATED/DELETED/TAG_LIST_RETRIEVED를 정의했다.
TagService 비즈니스 로직
src/main/java/.../tag/service/TagService.java
태그 생성 시 사용자 존재 및 중복명 검사 후 저장, 조회 시 접근 가능 태그 목록 변환, 삭제 시 유효성/기본태그금지/소유자 검증을 수행하는 로직을 구현했다.
요청/응답 DTO
src/main/java/.../tag/dto/request/TagCreateRequest.java, src/main/java/.../tag/dto/response/TagCreateResponse.java, src/main/java/.../tag/dto/response/TagListResponse.java
이름 유효성 검증이 포함된 요청 DTO와, Tag 엔티티를 변환하는 생성/목록 응답 DTO(from 팩토리 메서드 포함)를 추가했다.
컨트롤러 및 예외 핸들러
src/main/java/.../tag/controller/TagController.java, src/main/java/.../tag/exception/TagExceptionHandler.java
/api/v1/tags에 생성/조회/삭제 엔드포인트를 제공하는 TagController와, 유효성/타입 불일치 예외를 ErrorDto로 응답하는 TagExceptionHandler를 추가했다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant TagController
  participant TagService
  participant TagRepository
  Client->>TagController: POST /api/v1/tags (TagCreateRequest)
  TagController->>TagService: createTag(userId, request)
  TagService->>TagRepository: existsAccessibleByName(userId, name)
  TagRepository-->>TagService: false
  TagService->>TagRepository: saveAndFlush(Tag)
  TagRepository-->>TagService: Tag
  TagService-->>TagController: TagCreateResponse
  TagController-->>Client: BaseResponse(CREATED)
Loading
sequenceDiagram
  participant Client
  participant TagController
  participant TagService
  participant TagRepository
  Client->>TagController: DELETE /api/v1/tags/{tagId}
  TagController->>TagService: deleteTag(userId, tagId)
  TagService->>TagRepository: findById(tagId)
  TagRepository-->>TagService: Tag
  TagService->>TagService: isDefault, isOwnedBy 검증
  TagService->>TagRepository: delete(Tag)
  TagService-->>TagController: void
  TagController-->>Client: BaseResponse(DELETED)
Loading

Possibly related PRs

  • Team-Timo/Timo-Server#38: 동일한 Tag 도메인(엔티티, TagErrorCode, TagRepository)을 확장·오버홀하며 user_id/isDefault 로직을 다룬다.
  • Team-Timo/Timo-Server#49: Home의 TagResponse.from(Tag tag) 매핑에서 사용되는 Tag 엔티티(name, isDefault, 소유 매핑) 변경과 직접 연관된다.

Suggested labels: slack-approval-notified

Suggested reviewers: aneykrap

🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 태그 생성·삭제·조회 API 추가라는 주요 변경을 간단히 잘 요약합니다.
Linked Issues check ✅ Passed 태그 생성, 삭제, 목록 조회 API가 모두 구현되어 #79의 직접 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed 컨트롤러, 서비스, 엔티티, DTO, 마이그레이션 변경이 모두 태그 API 구현 범위 안에 있습니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#79-tag

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.

@github-actions github-actions Bot added ✨ feat and removed ✨ feat labels Jul 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@db/migration/2026-07-09__tags_add_owner_and_default.sql`:
- Around line 1-9: The migration only inserts rows into tags but does not apply
the schema changes the tags entity expects, so add the needed ALTER TABLE steps
before the INSERT. Update the tags table to add user_id, is_default, and the
name length/unique constraint required by the tags entity, then keep the seed
INSERT for the default tags so it runs against the corrected schema.

In `@src/main/java/com/Timo/Timo/domain/tag/dto/request/TagCreateRequest.java`:
- Around line 7-9: The TagCreateRequest.name validation is too permissive
compared to the Tag entity, so update the `@Size` constraint on TagCreateRequest
to match the entity’s `@Column`(length = 10) and change the validation message
accordingly. Keep the existing `@NotBlank` rule, and make sure the new max length
and message are consistent in the request DTO so TagCreateRequest rejects
overlong names before DB persistence.

In `@src/main/java/com/Timo/Timo/domain/tag/exception/TagErrorCode.java`:
- Around line 14-15: `TagErrorCode`의 `INVALID_REQUEST`와 `INVALID_TAG_ID`가 같은
code 값인 `TAG_400`을 공유해 클라이언트가 구분할 수 없습니다. `TagErrorCode` enum의 각 상수에 서로 다른 에러
코드를 부여하도록 수정하고, `INVALID_REQUEST`와 `INVALID_TAG_ID`의 의미를 각각 식별 가능한 고유 코드로 분리하세요.

In `@src/main/java/com/Timo/Timo/domain/tag/service/TagService.java`:
- Around line 28-42: The tag name length constraint is inconsistent between
TagCreateRequest and the Tag entity, causing valid DTO input to fail at save
time and be misreported as DUPLICATE_TAG_NAME in TagService.createTag. Update
TagCreateRequest so its `@Size` max matches the Tag entity’s `@Column`(length = 10),
keeping the validation aligned with the database limit and the existing
createTag flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 76059ac8-7176-4b20-8941-a2ac2ceae117

📥 Commits

Reviewing files that changed from the base of the PR and between 4688ca6 and b7c3b1e.

⛔ Files ignored due to path filters (1)
  • src/main/java/com/Timo/Timo/domain/tag/docs/TagControllerDocs.java is excluded by !**/docs/**
📒 Files selected for processing (11)
  • db/migration/2026-07-09__tags_add_owner_and_default.sql
  • src/main/java/com/Timo/Timo/domain/tag/controller/TagController.java
  • src/main/java/com/Timo/Timo/domain/tag/dto/request/TagCreateRequest.java
  • src/main/java/com/Timo/Timo/domain/tag/dto/response/TagCreateResponse.java
  • src/main/java/com/Timo/Timo/domain/tag/dto/response/TagListResponse.java
  • src/main/java/com/Timo/Timo/domain/tag/entity/Tag.java
  • src/main/java/com/Timo/Timo/domain/tag/exception/TagErrorCode.java
  • src/main/java/com/Timo/Timo/domain/tag/exception/TagExceptionHandler.java
  • src/main/java/com/Timo/Timo/domain/tag/exception/TagSuccessCode.java
  • src/main/java/com/Timo/Timo/domain/tag/repository/TagRepository.java
  • src/main/java/com/Timo/Timo/domain/tag/service/TagService.java

Comment on lines +1 to +9
-- 태그 소유자(user_id)와 기본 태그 여부(is_default) 컬럼 추가.
-- 운영은 ddl-auto: validate 이므로 배포 전에 반드시 먼저 실행해야 한다.

-- 모든 사용자가 공유하는 기본 태그. 소유자가 없으므로 user_id는 NULL이다.
INSERT INTO tags (user_id, name, is_default)
VALUES (NULL, '일상', b'1'),
(NULL, '운동', b'1'),
(NULL, '업무', b'1'),
(NULL, '과제', b'1');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: 기존 tags 테이블 생성 마이그레이션 및 name 컬럼 길이 확인

# 기존 tags 테이블 생성 마이그레이션 파일 확인
fd -e sql . db/migration/ | head -20

# tags 테이블 관련 기존 마이그레이션 내용 확인
rg -n -A5 'create table.*tags|CREATE TABLE.*tags|tags' db/migration/ --type sql

Repository: Team-Timo/Timo-Server

Length of output: 658


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== migration files =="
fd -e sql . db/migration | sort

echo
echo "== tags-related SQL occurrences =="
rg -n -A6 -B2 'create table.*tags|CREATE TABLE.*tags|alter table.*tags|tags' db/migration --type sql

echo
echo "== exact migration file =="
nl -ba db/migration/2026-07-09__tags_add_owner_and_default.sql

Repository: Team-Timo/Timo-Server

Length of output: 952


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search for tags entity / schema annotations =="
rg -n -A4 -B2 'class\s+Tag|interface\s+Tag|`@Table`\(name\s*=\s*"tags"|name\s*=\s*"tags"|`@Column`\(.*length\s*=\s*10|`@Column`\(.*length\s*=\s*20' . --glob '!**/target/**' --glob '!**/build/**' --glob '!**/dist/**'

echo
echo "== search for schema docs / ddl-auto mention =="
rg -n -A3 -B2 'ddl-auto|schema|tags' . --glob '!**/target/**' --glob '!**/build/**' --glob '!**/dist/**'

Repository: Team-Timo/Timo-Server

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
root = Path('db/migration')
for p in sorted(root.glob('*.sql')):
    print(p)
PY

Repository: Team-Timo/Timo-Server

Length of output: 215


INSERT 전에 ALTER TABLE를 추가하세요.
현재 파일에는 스키마 변경이 없고 tags 엔티티는 user_id, is_default, name(10), (user_id, name) 유니크 제약을 전제로 합니다. 이대로 배포하면 INSERT INTO tags (user_id, name, is_default)가 실패합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@db/migration/2026-07-09__tags_add_owner_and_default.sql` around lines 1 - 9,
The migration only inserts rows into tags but does not apply the schema changes
the tags entity expects, so add the needed ALTER TABLE steps before the INSERT.
Update the tags table to add user_id, is_default, and the name length/unique
constraint required by the tags entity, then keep the seed INSERT for the
default tags so it runs against the corrected schema.

Comment on lines +14 to +15
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "TAG_400", "태그 이름은 필수입니다."),
INVALID_TAG_ID(HttpStatus.BAD_REQUEST, "TAG_400", "유효하지 않은 태그 ID입니다."),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

INVALID_REQUESTINVALID_TAG_ID가 동일한 에러 코드 TAG_400을 사용합니다.

두 상수는 서로 다른 메시지를 가지지만 code 값이 "TAG_400"으로 동일하여, 클라이언트 측에서 에러 코드로 두 케이스를 구분할 수 없습니다. 로그 분석이나 클라이언트 에러 처리에 혼란을 줄 수 있습니다.

♻️ 제안하는 수정안
-	INVALID_REQUEST(HttpStatus.BAD_REQUEST, "TAG_400", "태그 이름은 필수입니다."),
-	INVALID_TAG_ID(HttpStatus.BAD_REQUEST, "TAG_400", "유효하지 않은 태그 ID입니다."),
+	INVALID_REQUEST(HttpStatus.BAD_REQUEST, "TAG_400_001", "태그 이름은 필수입니다."),
+	INVALID_TAG_ID(HttpStatus.BAD_REQUEST, "TAG_400_002", "유효하지 않은 태그 ID입니다."),
📝 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
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "TAG_400", "태그 이름은 필수입니다."),
INVALID_TAG_ID(HttpStatus.BAD_REQUEST, "TAG_400", "유효하지 않은 태그 ID입니다."),
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "TAG_400_001", "태그 이름은 필수입니다."),
INVALID_TAG_ID(HttpStatus.BAD_REQUEST, "TAG_400_002", "유효하지 않은 태그 ID입니다."),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/Timo/Timo/domain/tag/exception/TagErrorCode.java` around
lines 14 - 15, `TagErrorCode`의 `INVALID_REQUEST`와 `INVALID_TAG_ID`가 같은 code 값인
`TAG_400`을 공유해 클라이언트가 구분할 수 없습니다. `TagErrorCode` enum의 각 상수에 서로 다른 에러 코드를 부여하도록
수정하고, `INVALID_REQUEST`와 `INVALID_TAG_ID`의 의미를 각각 식별 가능한 고유 코드로 분리하세요.

Comment on lines +28 to +42
public TagCreateResponse createTag(Long userId, TagCreateRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));

if (tagRepository.existsAccessibleByName(userId, request.name())) {
throw new CustomException(TagErrorCode.DUPLICATE_TAG_NAME);
}

try {
Tag tag = tagRepository.saveAndFlush(Tag.ofUser(user, request.name()));
return TagCreateResponse.from(tag);
} catch (DataIntegrityViolationException exception) {
throw new CustomException(TagErrorCode.DUPLICATE_TAG_NAME);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

태그명 글자수 제한이 DTO와 Entity 간에 불일치합니다.

TagCreateRequest@Size(max = 20)Tag 엔티티의 @Column(length = 10)이 일치하지 않습니다. 11~20자 사이의 태그명은 DTO 검증을 통과하지만 DB 저장 시 DataIntegrityViolationException이 발생하여 DUPLICATE_TAG_NAME 에러로 잘못 매핑됩니다. PR 커밋 메시지에 "태그명 글자수 10개 제한 변경"이 명시되어 있으므로, DTO의 max 값을 10으로 수정해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/Timo/Timo/domain/tag/service/TagService.java` around lines
28 - 42, The tag name length constraint is inconsistent between TagCreateRequest
and the Tag entity, causing valid DTO input to fail at save time and be
misreported as DUPLICATE_TAG_NAME in TagService.createTag. Update
TagCreateRequest so its `@Size` max matches the Tag entity’s `@Column`(length = 10),
keeping the validation aligned with the database limit and the existing
createTag flow.

@github-actions github-actions Bot added ✨ feat and removed ✨ feat labels Jul 9, 2026

@Jy000n Jy000n left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

깔끔한 것 같습니다..!! 수고하셨습니다아

@aneykrap aneykrap left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

태그 빠잉


@Query("""
select t from Tag t
where t.isDefault = true or t.user.id = :userId

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

기본 태그와 사용자 태그를 같은 조회 조건으로 묶어서 가져와서 깔끔쓰한듯 보입니다 굿굿

throw new CustomException(TagErrorCode.TAG_NOT_FOUND);
}

tagRepository.delete(tag);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

p5) 갑자기 궁금해진 부분인데 삭제하려는 태그가 이미 TODO에서 사용 중인 경우에는 어떻게 처리되나요?

현재 플로우에서는 태그 사용 여부를 확인하지 않고 tagRepository.delete(tag)만 호출해서 기존 TODO의 tagId는 그대로 남아있는걸로 이해했는데 이후 조회 시 해당 태그를 찾지 못해 null로 내려가면 태그가 없는 TODO로 처리되는 건가용??

@github-actions github-actions Bot added the slack-approval-notified Slack 승인 완료 알림 중복 방지용 라벨 label Jul 9, 2026
@laura-jung laura-jung merged commit c12e3bf into develop Jul 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat slack-approval-notified Slack 승인 완료 알림 중복 방지용 라벨 🍀 윤아

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 태그 api

3 participants