-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 유저 태그 도메인 설계 및 유저 태그 생성 API #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
328852d
479b3b6
f65b9d6
1702c6c
88eaad7
25891a8
f682e56
bbe6383
9eb0a3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package com.capturecat.core.api.user; | ||
|
|
||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| import com.capturecat.core.service.auth.LoginUser; | ||
| import com.capturecat.core.service.image.TagResponse; | ||
| import com.capturecat.core.service.user.UserTagService; | ||
| import com.capturecat.core.support.response.ApiResponse; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/v1/user-tags") | ||
| public class UserTagController { | ||
|
|
||
| private final UserTagService userTagService; | ||
|
|
||
| @PostMapping | ||
| public ApiResponse<TagResponse> create(@AuthenticationPrincipal LoginUser loginUser, @RequestParam String tagName) { | ||
| TagResponse tagResponse = userTagService.create(loginUser, tagName); | ||
|
|
||
| return ApiResponse.success(tagResponse); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,13 @@ | ||
| package com.capturecat.core.domain.tag; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface TagRepository extends JpaRepository<Tag, Long>, TagCustomRepository { | ||
|
|
||
| List<Tag> findByNameIn(List<String> names); | ||
|
|
||
| Optional<Tag> findByName(String name); | ||
|
Comment on lines
10
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enforce uniqueness on Tag.name (ideally case-insensitive) to make findByName reliable and avoid races. Consider adding DB constraints (PostgreSQL examples): -- strict case-sensitive uniqueness
ALTER TABLE tag ADD CONSTRAINT uk_tag_name UNIQUE (name);
-- or case-insensitive uniqueness
CREATE UNIQUE INDEX ux_tag_name_ci ON tag (lower(name));🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package com.capturecat.core.domain.user; | ||
|
|
||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import com.capturecat.core.domain.BaseTimeEntity; | ||
| import com.capturecat.core.domain.tag.Tag; | ||
|
|
||
| @Entity | ||
| @Table(name = "user_tag", | ||
| uniqueConstraints = @UniqueConstraint(name = "uk_user_tag_user_tag", columnNames = {"user_id", "tag_id"}) | ||
| ) | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Getter | ||
| public class UserTag extends BaseTimeEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "user_id") | ||
| private User user; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "tag_id") | ||
| private Tag tag; | ||
|
|
||
| private UserTag(User user, Tag tag) { | ||
| this.user = user; | ||
| this.tag = tag; | ||
| } | ||
|
|
||
| public static UserTag create(User user, Tag tag) { | ||
| return new UserTag(user, tag); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.capturecat.core.domain.user; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import com.capturecat.core.domain.tag.Tag; | ||
|
|
||
| public interface UserTagRepository extends JpaRepository<UserTag, Long> { | ||
|
|
||
| boolean existsByUserAndTag(User user, Tag tag); | ||
|
|
||
| long countByUser(User user); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,67 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package com.capturecat.core.service.user; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.dao.DataIntegrityViolationException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.domain.tag.Tag; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.domain.tag.TagRegister; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.domain.user.User; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.domain.user.UserRepository; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.domain.user.UserTag; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.domain.user.UserTagRepository; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.service.auth.LoginUser; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.service.image.TagResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.support.error.CoreException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.capturecat.core.support.error.ErrorType; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class UserTagService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final int MAX_USER_TAG_COUNT = 30; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final UserRepository userRepository; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final UserTagRepository userTagRepository; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final TagRegister tagRegister; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public TagResponse create(LoginUser loginUser, String tagName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| User user = userRepository.findByUsername(loginUser.getUsername()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Tag tag = tagRegister.registerTagsFor(tagName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validate(user, tag); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userTagRepository.save(UserTag.create(user, tag)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return TagResponse.from(tag); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (DataIntegrityViolationException ex) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new CoreException(ErrorType.USER_TAG_ALREADY_EXISTS); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainEnsure DB constraints back these checks. Validate that user_tag has (user_id, tag_id) unique + FKs and relevant indexes; also that tag.name is unique (TagRegister relies on it). 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "user_tag DDL:"
fd -a 'V*__*user_tag*.sql' capturecat-core/src/main/resources/db/migration | xargs -I{} sh -c 'echo "== {} =="; cat "{}"'
echo
echo "[Checks]"
rg -nP 'unique\s*\(\s*user_id\s*,\s*tag_id\s*\)' capturecat-core/src/main/resources/db/migration || echo "Missing composite UNIQUE (user_id, tag_id)"
rg -nP 'foreign key.*user_id' capturecat-core/src/main/resources/db/migration || echo "Missing FK for user_id"
rg -nP 'foreign key.*tag_id' capturecat-core/src/main/resources/db/migration || echo "Missing FK for tag_id"
rg -nP '(created_|last_modified_)' capturecat-core/src/main/resources/db/migration/*user_tag*.sql || echo "Missing auditing timestamps on user_tag"
rg -nP 'index.*user_id' capturecat-core/src/main/resources/db/migration/*user_tag*.sql || echo "Consider index on user_id"
rg -nP 'index.*tag_id' capturecat-core/src/main/resources/db/migration/*user_tag*.sql || echo "Consider index on tag_id"
echo
echo "tag DDL (checking unique name):"
fd -a 'V*__*.sql' capturecat-core/src/main/resources/db/migration | xargs rg -nP 'create\s+table.*tag|unique.*\(name\)' -n -C1Length of output: 2614 Add UNIQUE constraint on tag.name 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+32
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Catch DataIntegrityViolationException only around save(). Avoid mis-mapping integrity errors thrown by unrelated operations. @@
- @Transactional
- public TagResponse create(LoginUser loginUser, String tagName) {
- try {
- User user = userRepository.findByUsername(loginUser.getUsername())
- .orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND));
- Tag tag = tagRegister.registerTagsFor(tagName);
-
- validate(user, tag);
-
- userTagRepository.save(UserTag.create(user, tag));
-
- return TagResponse.from(tag);
- } catch (DataIntegrityViolationException ex) {
- throw new CoreException(ErrorType.USER_TAG_ALREADY_EXISTS);
- }
- }
+ @Transactional
+ public TagResponse create(LoginUser loginUser, String tagName) {
+ User user = userRepository.findByUsername(loginUser.getUsername())
+ .orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND));
+ Tag tag = tagRegister.registerTagsFor(tagName);
+
+ validate(user, tag);
+
+ try {
+ userTagRepository.save(UserTag.create(user, tag));
+ } catch (DataIntegrityViolationException ex) {
+ throw new CoreException(ErrorType.USER_TAG_ALREADY_EXISTS);
+ }
+
+ return TagResponse.from(tag);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void validate(User user, Tag tag) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validateDuplicateUserTag(user, tag); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validateUserTagCountLimit(user); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void validateDuplicateUserTag(User user, Tag tag) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (userTagRepository.existsByUserAndTag(user, tag)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new CoreException(ErrorType.USER_TAG_ALREADY_EXISTS); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void validateUserTagCountLimit(User user) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| long userTagCount = userTagRepository.countByUser(user); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (userTagCount >= MAX_USER_TAG_COUNT) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new CoreException(ErrorType.TOO_MANY_USER_TAGS); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,87 @@ | ||||||||||||||||||||||||||||||||||||||||||
| create table if not exists users | ||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||
| id bigint not null primary key, | ||||||||||||||||||||||||||||||||||||||||||
| nickname varchar(50) not null, | ||||||||||||||||||||||||||||||||||||||||||
| email varchar(50) not null, | ||||||||||||||||||||||||||||||||||||||||||
| username varchar(50) not null unique, | ||||||||||||||||||||||||||||||||||||||||||
| password varchar(70), | ||||||||||||||||||||||||||||||||||||||||||
| role varchar(255) not null constraint users_role_check check (role in ('ADMIN', 'PREMIUM_USER', 'USER')), | ||||||||||||||||||||||||||||||||||||||||||
| tutorial_completed boolean not null, | ||||||||||||||||||||||||||||||||||||||||||
| provider varchar(255), | ||||||||||||||||||||||||||||||||||||||||||
| social_id varchar(255), | ||||||||||||||||||||||||||||||||||||||||||
| created_date timestamp(6) not null, | ||||||||||||||||||||||||||||||||||||||||||
| last_modified_date timestamp(6) not null | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create table if not exists user_social_account | ||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||
| id bigint not null primary key, | ||||||||||||||||||||||||||||||||||||||||||
| created_date timestamp(6) not null, | ||||||||||||||||||||||||||||||||||||||||||
| last_modified_date timestamp(6) not null, | ||||||||||||||||||||||||||||||||||||||||||
| provider varchar(30) not null, | ||||||||||||||||||||||||||||||||||||||||||
| social_id varchar(100) not null, | ||||||||||||||||||||||||||||||||||||||||||
| unlink_key varchar(512), | ||||||||||||||||||||||||||||||||||||||||||
| user_id bigint not null constraint fk998rgv7jn090iyc77f8e1xsnq references users, | ||||||||||||||||||||||||||||||||||||||||||
| constraint uksj2lqxj8h0xuqf9v1dvtlkegt unique (provider, social_id) | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create table if not exists refresh_token | ||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||
| id bigint not null primary key, | ||||||||||||||||||||||||||||||||||||||||||
| refresh_token_expiration bigint not null, | ||||||||||||||||||||||||||||||||||||||||||
| expiration varchar(255), | ||||||||||||||||||||||||||||||||||||||||||
| refresh_token varchar(255), | ||||||||||||||||||||||||||||||||||||||||||
| username varchar(255) | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion refresh_token: ambiguous expirations and missing FK to user.
-create table if not exists refresh_token
+create table if not exists refresh_token
(
- id bigint not null primary key,
- refresh_token_expiration bigint not null,
- expiration varchar(255),
+ id bigint generated by default as identity primary key,
+ refresh_token_expiration bigint not null,
refresh_token varchar(255),
- username varchar(255)
+ username varchar(50) not null,
+ constraint fk_refresh_token_user_username foreign key (username) references users(username) on delete cascade
);If you truly need two expirations, rename with clear semantics. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create table if not exists withdraw_log | ||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||
| id bigint generated by default as identity primary key, | ||||||||||||||||||||||||||||||||||||||||||
| created_date timestamp(6) not null, | ||||||||||||||||||||||||||||||||||||||||||
| last_modified_date timestamp(6) not null, | ||||||||||||||||||||||||||||||||||||||||||
| image_cleanup_status varchar(255) not null constraint withdraw_log_image_cleanup_status_check check (image_cleanup_status in ('PENDING', 'DONE', 'FAILED')), | ||||||||||||||||||||||||||||||||||||||||||
| reason text, | ||||||||||||||||||||||||||||||||||||||||||
| user_id bigint not null | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion withdraw_log: add FK for user_id. Currently no FK; risks orphaned rows. create table if not exists withdraw_log
(
id bigint generated by default as identity primary key,
created_date timestamp(6) not null,
last_modified_date timestamp(6) not null,
image_cleanup_status varchar(255) not null constraint withdraw_log_image_cleanup_status_check check (image_cleanup_status in ('PENDING', 'DONE', 'FAILED')),
reason text,
- user_id bigint not null
+ user_id bigint not null constraint fk_withdraw_log_user references users
);Optionally add 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create index if not exists idx_withdraw_log_user_id on withdraw_log (user_id); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create index if not exists idx_withdraw_log_created_date on withdraw_log (created_date); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create index if not exists idx_withdraw_log_cleanup_status on withdraw_log (image_cleanup_status, created_date); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create table if not exists images | ||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||
| id bigint generated by default as identity primary key, | ||||||||||||||||||||||||||||||||||||||||||
| size bigint, | ||||||||||||||||||||||||||||||||||||||||||
| file_name varchar(255), | ||||||||||||||||||||||||||||||||||||||||||
| file_url varchar(255), | ||||||||||||||||||||||||||||||||||||||||||
| capture_date date, | ||||||||||||||||||||||||||||||||||||||||||
| created_date timestamp(6) not null, | ||||||||||||||||||||||||||||||||||||||||||
| last_modified_date timestamp(6) not null, | ||||||||||||||||||||||||||||||||||||||||||
| user_id bigint constraint fk13ljqfrfwbyvnsdhihwta8cpr references users | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create table if not exists tag | ||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||
| id bigint generated by default as identity primary key, | ||||||||||||||||||||||||||||||||||||||||||
| name varchar(255), | ||||||||||||||||||||||||||||||||||||||||||
| created_date timestamp(6) not null, | ||||||||||||||||||||||||||||||||||||||||||
| last_modified_date timestamp(6) not null | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create table if not exists image_tag | ||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||
| id bigint generated by default as identity primary key, | ||||||||||||||||||||||||||||||||||||||||||
| image_id bigint constraint fk6q9wuvp5j846qtqod6xu3gma1 references images, | ||||||||||||||||||||||||||||||||||||||||||
| tag_id bigint constraint fk28yowgjl7oksr7dc0wj7f5il references tag, | ||||||||||||||||||||||||||||||||||||||||||
| created_date timestamp(6) not null, | ||||||||||||||||||||||||||||||||||||||||||
| last_modified_date timestamp(6) not null | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+73
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion image_tag: prevent duplicates and add indexes.
create table if not exists image_tag
(
id bigint generated by default as identity primary key,
- image_id bigint constraint fk6q9wuvp5j846qtqod6xu3gma1 references images,
- tag_id bigint constraint fk28yowgjl7oksr7dc0wj7f5il references tag,
+ image_id bigint not null constraint fk6q9wuvp5j846qtqod6xu3gma1 references images,
+ tag_id bigint not null constraint fk28yowgjl7oksr7dc0wj7f5il references tag,
created_date timestamp(6) not null,
last_modified_date timestamp(6) not null
);
+
+create unique index if not exists uk_image_tag_image_tag on image_tag (image_id, tag_id);
+create index if not exists idx_image_tag_image_id on image_tag (image_id);
+create index if not exists idx_image_tag_tag_id on image_tag (tag_id);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| create table if not exists bookmark | ||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||
| id bigint generated by default as identity primary key, | ||||||||||||||||||||||||||||||||||||||||||
| image_id bigint constraint fkpowbsxsu0qwcon1yoxbsqkw4w references images, | ||||||||||||||||||||||||||||||||||||||||||
| user_id bigint constraint fko4vbqvq5trl11d85bqu5kl870 references users | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+82
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion bookmark: prevent duplicates and add indexes. Enforce one bookmark per (user, image) and speed lookups. create table if not exists bookmark
(
id bigint generated by default as identity primary key,
image_id bigint constraint fkpowbsxsu0qwcon1yoxbsqkw4w references images,
user_id bigint constraint fko4vbqvq5trl11d85bqu5kl870 references users
);
+
+create unique index if not exists uk_bookmark_user_image on bookmark (user_id, image_id);
+create index if not exists idx_bookmark_image_id on bookmark (image_id);
+create index if not exists idx_bookmark_user_id on bookmark (user_id);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,9 @@ | ||||||
| create table if not exists user_tag | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid IF NOT EXISTS in versioned migrations. Flyway migrations should be deterministic; IF NOT EXISTS can hide drift and let incompatible schemas slip through. -create table if not exists user_tag
+create table user_tagIf the table might already exist in some envs, prefer a separate repeatable migration or conditional precheck script, not IF NOT EXISTS in V2. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| ( | ||||||
| id bigint generated by default as identity primary key, | ||||||
| user_id bigint constraint fk_user_tag_user references users, | ||||||
| tag_id bigint constraint fk_user_tag_tag references tag, | ||||||
|
Comment on lines
+4
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion FKs should be NOT NULL and consider ON DELETE CASCADE. A user_tag without user/tag is invalid. Also consider cascading deletes to avoid orphans when a user/tag is removed. Option A (edit V2 before merge): - user_id bigint constraint fk_user_tag_user references users,
- tag_id bigint constraint fk_user_tag_tag references tag,
+ user_id bigint not null
+ constraint fk_user_tag_user references users(id) on delete cascade,
+ tag_id bigint not null
+ constraint fk_user_tag_tag references tag(id) on delete cascade,If V2 may already be applied locally, create V3 with: alter table user_tag
alter column user_id set not null,
alter column tag_id set not null;
alter table user_tag
drop constraint if exists fk_user_tag_user,
add constraint fk_user_tag_user foreign key (user_id) references users(id) on delete cascade;
alter table user_tag
drop constraint if exists fk_user_tag_tag,
add constraint fk_user_tag_tag foreign key (tag_id) references tag(id) on delete cascade;🤖 Prompt for AI Agents |
||||||
| created_date timestamp(6) not null, | ||||||
| last_modified_date timestamp(6) not null, | ||||||
| constraint uk_user_tag_user_tag unique (user_id, tag_id) | ||||||
| ); | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.capturecat.core.api.user; | ||
|
|
||
| import static com.capturecat.core.support.error.ErrorType.TOO_MANY_USER_TAGS; | ||
| import static com.capturecat.core.support.error.ErrorType.USER_NOT_FOUND; | ||
| import static com.capturecat.core.support.error.ErrorType.USER_TAG_ALREADY_EXISTS; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import com.capturecat.core.api.error.ErrorCodeDocumentTest; | ||
| import com.capturecat.test.snippet.ErrorCodeDescriptor; | ||
|
|
||
| class UserErrorCodeControllerTest extends ErrorCodeDocumentTest { | ||
|
|
||
| @Test | ||
| void 유저_태그_생성_에러_코드_문서화() { | ||
| List<ErrorCodeDescriptor> errorCodeDescriptors = generateErrorCodeDescriptors(USER_TAG_ALREADY_EXISTS, | ||
| TOO_MANY_USER_TAGS, USER_NOT_FOUND); | ||
| generateErrorDocs("errorCode/createUserTag", errorCodeDescriptors); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Make single-tag register resilient to concurrent inserts.
If another transaction inserts the same tag between find and save, re-read after the unique-constraint violation.
@Transactional public Tag registerTagsFor(String tagName) { - return tagRepository.findByName(tagName) - .orElseGet(() -> tagRepository.save(new Tag(tagName))); + return tagRepository.findByName(tagName) + .orElseGet(() -> { + try { + return tagRepository.save(new Tag(tagName)); + } catch (DataIntegrityViolationException e) { + // Another transaction likely created it; fetch and return. + return tagRepository.findByName(tagName).orElseThrow(() -> e); + } + }); }If not already present, ensure a unique index on
tag.name(preferably case-insensitive) in Flyway migrations to make this effective.To verify constraints exist in migrations, you can run:
🏁 Script executed:
Length of output: 475
Enforce unique tag.name constraint and make registerTagsFor concurrency-safe
TagRegister.registerTagsFor, wraptagRepository.save(new Tag(tagName))in a try/catch forDataIntegrityViolationExceptionand re-fetch the existing tag when a unique-constraint violation occurs (as in the suggested diff).tag.name(preferably case-insensitive) to prevent duplicate inserts.🤖 Prompt for AI Agents