From f1dcf2a8ff60708a1887c818499866cf493a4093 Mon Sep 17 00:00:00 2001 From: Hwan0518 Date: Tue, 27 Jan 2026 18:42:43 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20CLAUDE.md=EC=99=80=20SKILL.md=EB=A5=BC?= =?UTF-8?q?=20=EC=A2=80=20=EB=8D=94=20=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/GUIDELINES.md | 4 +- .claude/skills/domain-persistence/SKILL.md | 942 +++--------------- .claude/skills/domain-persistence/examples.md | 545 ++++++++++ .../skills/domain-persistence/reference.md | 128 +++ .claude/skills/test-code/SKILL.md | 2 +- .claude/skills/web-layer/SKILL.md | 22 +- CLAUDE.md | 10 +- 7 files changed, 843 insertions(+), 810 deletions(-) create mode 100644 .claude/skills/domain-persistence/examples.md create mode 100644 .claude/skills/domain-persistence/reference.md diff --git a/.claude/skills/GUIDELINES.md b/.claude/skills/GUIDELINES.md index 2ab08e8..a8fc16b 100644 --- a/.claude/skills/GUIDELINES.md +++ b/.claude/skills/GUIDELINES.md @@ -412,7 +412,7 @@ public String field; // ❌ Breaks encapsulation ```markdown | Layer | Pattern | Example | Location | |-------|---------|---------|----------| -| **Domain Model** | Singular noun | `User`, `Item` | `[domain]/domain/model/` | +| **Domain Model** | Plural noun | `Users`, `Items` | `[domain]/domain/model/` | | **Persistence Entity** | Noun + "Entity" | `UserEntity` | `[domain]/adapter/.../entity/` | ``` @@ -717,7 +717,7 @@ repository.deleteAll(); NEVER use deleteAllInBatch() - can delete production data! -**2026년 1월 Incident**: deleteAllInBatch() wiped entire database. +**2026년 1월경 실제 사고**: deleteAllInBatch() wiped entire database. ❌ FORBIDDEN: ```java diff --git a/.claude/skills/domain-persistence/SKILL.md b/.claude/skills/domain-persistence/SKILL.md index db98fde..f79baaf 100644 --- a/.claude/skills/domain-persistence/SKILL.md +++ b/.claude/skills/domain-persistence/SKILL.md @@ -56,13 +56,81 @@ Activate this skill when: | Layer | Pattern | Example | Location | |-------|---------|---------|----------| -| **Domain Model** | Singular noun | `Users`, `Item`, `Trade` | `[domain]/domain/model/` | +| **Domain Model** | Plural noun | `Users`, `Items`, `Trades` | `[domain]/domain/model/` | | **Domain VO** | Descriptive name | `BranchOption`, `UserAddress` | `[domain]/domain/model/vo/` | | **Persistence Entity** | Noun + "Entity" | `UserEntity`, `ItemEntity` | `[domain]/adapter/out/persistence/entity/` | | **Persistence VO** | Name + "Vo" | `BranchOptionVo`, `UserAddressVo` | `[domain]/adapter/out/persistence/entity/vo/` | --- +# Workflow + +## Step 1: Classify Components + +**DO:** +- Decide which classes are Domain Models, Entities, and VOs +- Confirm which references must be ID-only in domain models + +**DO NOT:** +- Mix JPA annotations into domain models +- Use object references in domain models + +**Verify:** +- [ ] Domain vs persistence boundaries are clear + +## Step 2: Implement Domain Models + +**DO:** +- Keep POJO-only domain models +- Add static factory methods and business logic +- Manage timestamps in domain layer + +**DO NOT:** +- Add @Entity/@Table/@Column to domain models + +**Verify:** +- [ ] Domain model checklist passes + +## Step 3: Implement Persistence Entities + +**DO:** +- Use proper JPA annotations +- Extend BaseTimeEntity +- Define relationships only in entities + +**DO NOT:** +- Add business logic methods + +**Verify:** +- [ ] Persistence entity checklist passes + +## Step 4: Implement VOs and Relationships + +**DO:** +- Domain VO: no @Embeddable +- Persistence VO: @Embeddable with @Column +- Use FetchType.LAZY + +**DO NOT:** +- Add relationship objects to domain models + +**Verify:** +- [ ] VO and relationship rules are satisfied + +## Step 5: Repository Conversion & Type Audit + +**DO:** +- Convert Domain ↔ Entity via ModelMapper in repositories +- Enforce UUID BINARY(16) and numeric type rules + +**DO NOT:** +- Bypass conversion or reuse entity in domain + +**Verify:** +- [ ] Repository and type rules are satisfied + +--- + # Domain Model Structure (MUST) ## Required Annotations @@ -71,7 +139,7 @@ Activate this skill when: @Getter @NoArgsConstructor @Builder(toBuilder = true) -public class Item { +public class Items { // Fields with NO JPA annotations } ``` @@ -84,55 +152,7 @@ public class Item { - ✅ Include **business logic methods** (validation, calculations, state changes) - ✅ Manual timestamp management (createdAt, updatedAt, deletedAt for soft delete) -## Example - Item Domain Model - -```java -@Getter -@NoArgsConstructor -@Builder(toBuilder = true) -public class Item { - private Long id; - private String itemName; - private String itemCode; - private Boolean isEnabled; - private Integer itemTypeId; // ← ID reference, not object - private Integer unitPrice; - private String unit; - private Long carbonEmissionFactorId; // ← ID reference, not object - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - // ✅ Static factory method - public static Item from(CreateItemCommand command) { - return Item.builder() - .itemName(command.getItemName()) - .itemCode(command.getItemCode()) - .isEnabled(command.getIsEnabled()) - .itemTypeId(command.getItemTypeId()) - .unitPrice(command.getUnitPrice()) - .unit(command.getUnit()) - .carbonEmissionFactorId(command.getCarbonEmissionFactorId()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - } - - // ✅ Business logic method - public void updateItemInfo(UpdateItemCommand command) { - this.itemName = command.getItemName(); - this.itemCode = command.getItemCode(); - this.isEnabled = command.getIsEnabled(); - this.updatedAt = LocalDateTime.now(); - } - - // ✅ Validation method - public void validateEnabled() { - if (!this.isEnabled) { - throw new BaseException(BaseResponseStatus.ITEM_DISABLED); - } - } -} -``` +**Examples:** See `examples.md#domain-model-example`. --- @@ -160,44 +180,7 @@ public class ItemEntity extends BaseTimeEntity { - ❌ NO business logic methods - ✅ Protected no-args constructor + private all-args constructor -## Example - ItemEntity Persistence Model - -```java -@Entity -@Table(name = "item") -@Getter -@Builder(toBuilder = true) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class ItemEntity extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "item_id") - private Long id; - - @Column(name = "item_name", length = 100, nullable = false) - private String itemName; - - @Column(name = "item_code", length = 50, unique = true, nullable = false) - private String itemCode; - - @Column(name = "is_enabled", nullable = false) - private Boolean isEnabled; - - @Column(name = "item_type_id") - private Integer itemTypeId; - - @Column(name = "unit_price") - private Integer unitPrice; - - @Column(name = "unit", length = 20) - private String unit; - - @Column(name = "carbon_emission_factor_id") - private Long carbonEmissionFactorId; -} -``` +**Examples:** See `examples.md#persistence-entity-example`. --- @@ -209,50 +192,13 @@ public class ItemEntity extends BaseTimeEntity { - ✅ Simple @Getter, @NoArgsConstructor, @Builder - ✅ Validation annotations (@NotNull, @NotBlank) -```java -@Getter -@NoArgsConstructor -@Builder -public class BranchOption { - @NotNull - private Long branchId; - - @NotBlank - private String branchName; -} -``` - ## Persistence Value Objects (`[domain]/adapter/out/persistence/entity/vo/`) - ✅ MUST use @Embeddable annotation - ✅ Protected/private constructors - ✅ @Column annotations for each field -```java -@Getter -@Builder -@Embeddable -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class BranchOptionVo { - @Column(name = "branch_id") - private Long branchId; - - @Column(name = "branch_name", length = 100) - private String branchName; -} -``` - -## Usage in Entity - -```java -@Entity -@Table(name = "users") -public class UserEntity extends BaseTimeEntity { - @Embedded - private BranchOptionVo branchOption; // ← Use Persistence VO -} -``` +**Examples:** See `examples.md#value-objects`. --- @@ -264,43 +210,13 @@ public class UserEntity extends BaseTimeEntity { ## Domain Models Use IDs -```java -@Getter -@Builder -public class TradeItem { - private Long id; - private Long tradeId; // ← Just the ID - private Long itemId; // ← Just the ID - private Integer quantity; -} -``` +- Domain models reference related entities by ID only +- No object references in domain models ## Persistence Entities Define Relationships -```java -@Entity -@Table(name = "trade_item") -public class TradeItemEntity extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) // ← Relationship only in Entity - @JoinColumn(name = "trade_id", nullable = false) - private TradeEntity trade; - - @Column(name = "item_id") - private Long itemId; - - @Column(name = "quantity") - private Integer quantity; - - // Helper method to extract ID for domain conversion - public Long getTradeId() { - return trade != null ? trade.getId() : null; - } -} -``` +- Use @ManyToOne / @OneToMany only in entities +- Provide helper methods to extract IDs during conversion ## Relationship Guidelines (MUST) @@ -309,278 +225,51 @@ public class TradeItemEntity extends BaseTimeEntity { - ✅ Provide helper methods to extract IDs when needed - ❌ Avoid bidirectional mappings unless absolutely necessary +**Examples:** See `examples.md#relationship-mapping`. + --- # Repository Conversion Pattern (MUST) -## Command Repository (save operations) - -```java -@Repository -@RequiredArgsConstructor -public class ItemCommandRepositoryImpl implements ItemCommandRepository { - private final ItemJpaRepository itemJpaRepository; - private final ModelMapper modelMapper; - - @Override - public Item save(Item item) { - // Domain → Entity - ItemEntity itemEntity = modelMapper.map(item, ItemEntity.class); - - // Persist - ItemEntity savedEntity = itemJpaRepository.save(itemEntity); - - // Entity → Domain - return modelMapper.map(savedEntity, Item.class); - } -} -``` - -## Query Repository (read operations) +- Command repositories: Domain → Entity → save → Domain +- Query repositories: Entity → Domain (throw BaseException when not found) +- All conversions are explicit and use ModelMapper -```java -@Repository -@RequiredArgsConstructor -public class ItemQueryRepositoryImpl implements ItemQueryRepository { - private final ItemJpaRepository itemJpaRepository; - private final ModelMapper modelMapper; - - @Override - public Item findById(Long id) { - ItemEntity entity = itemJpaRepository.findById(id) - .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_EXIST_ITEM)); - - // Entity → Domain - return modelMapper.map(entity, Item.class); - } - - @Override - public List findAll() { - return itemJpaRepository.findAll().stream() - .map(entity -> modelMapper.map(entity, Item.class)) - .toList(); - } -} -``` +**Examples:** See `examples.md#repository-conversion-pattern`. --- # UUID and Numeric Type Handling (CRITICAL) -## UUID Fields (MUST use BINARY(16)) - -All UUID fields in persistence entities MUST use `BINARY(16)` column definition: - -**Persistence Entity:** -```java -@Entity -@Table(name = "users") -public class UserEntity extends BaseTimeEntity { - @Column(name = "user_uuid", nullable = false, columnDefinition = "BINARY(16)", unique = true) - private UUID userUuid; -} - -@Entity -@Table(name = "trade") -public class TradeEntity extends BaseTimeEntity { - @Column(name = "seller_uuid", columnDefinition = "BINARY(16)", nullable = false) - private UUID sellerUuid; - - @Column(name = "buyer_uuid", columnDefinition = "BINARY(16)", nullable = false) - private UUID buyerUuid; -} -``` - -**Domain Model:** -```java -@Getter -@Builder(toBuilder = true) -public class Users { - private UUID userUuid; // ← Plain UUID, no annotations -} - -@Getter -@Builder(toBuilder = true) -public class Trade { - private UUID sellerUuid; - private UUID buyerUuid; -} -``` - -## BigDecimal Usage (for decimals) - -Use `BigDecimal` for: -- **Weights** (무게): `totalWeight`, `payloadWeight`, `curbWeight` -- **Rates/Ratios** (비율): `totalWeightLossRate`, `emissionFactorKgCo2ePerKg` -- **Carbon values** (탄소 관련): `carbonReductionAmount` -- **Prices with decimals** (소수점 가격): `userTradeItemUnitPrice` - -**Entity:** -```java -@Entity -@Table(name = "trade") -public class TradeEntity extends BaseTimeEntity { - @Column(name = "total_weight", precision = 10, scale = 2) - private BigDecimal totalWeight; - - @Column(name = "total_weight_loss_rate", precision = 5, scale = 2) - private BigDecimal totalWeightLossRate; - - @Column(name = "carbon_reduction_amount", precision = 10, scale = 2) - private BigDecimal carbonReductionAmount; -} -``` - -**Domain Model:** -```java -@Getter -@Builder(toBuilder = true) -public class Trade { - private BigDecimal totalWeight; - private BigDecimal totalWeightLoss; - private BigDecimal totalWeightLossRate; - private BigDecimal carbonReductionAmount; - - // ✅ Business logic using BigDecimal - public BigDecimal calculateWeightLoss() { - if (totalWeight != null && payloadWeight != null) { - return totalWeight.subtract(payloadWeight); - } - return BigDecimal.ZERO; // ← Use ZERO constant - } -} -``` - -## BigInteger Usage (for large integers) - -Use `BigInteger` for: -- **Monetary amounts** (금액): `tradeAmount`, `totalBuyAmount`, `totalSellAmount` -- **Large integer values** without decimal points - -```java -@Entity -@Table(name = "trade") -public class TradeEntity extends BaseTimeEntity { - @Column(name = "trade_amount", nullable = false) - private BigInteger tradeAmount; // ← 금액은 BigInteger -} -``` - -## Integer Usage (for counts/small values) - -Use `Integer` for: -- **Counts**: `totalBuyCount`, `totalSellCount` -- **Unit prices** when stored as integers: `unitPrice` -- **Small numeric IDs or codes** +- UUID fields in persistence entities **MUST** use `BINARY(16)` +- Use `BigDecimal` for weights/rates/carbon and `BigInteger` for monetary amounts +- Use `Integer` for counts and small numeric values +- BigDecimal rules: `BigDecimal.ZERO`, `compareTo()`, precision/scale, null checks -## BigDecimal Guidelines (MUST) - -- ✅ Use `BigDecimal.ZERO` instead of `new BigDecimal(0)` for zero values -- ✅ Always use `compareTo()` for BigDecimal comparisons, **NOT `equals()`** -- ✅ Specify `precision` and `scale` in @Column for BigDecimal fields -- ✅ Handle null checks before BigDecimal arithmetic operations - -**Example:** -```java -// ❌ WRONG -if (amount.equals(BigDecimal.ZERO)) { ... } - -// ✅ CORRECT -if (amount.compareTo(BigDecimal.ZERO) == 0) { ... } - -// ✅ CORRECT -if (amount1.compareTo(amount2) > 0) { ... } // amount1 > amount2 -``` +**Details & code samples:** See `reference.md#uuid-and-numeric-type-handling`. --- # Static Factory Methods (MUST) -Use static factory methods for domain model creation: +- Use static factory methods (`from`, `of`, `create`) in domain models +- Set timestamps in domain layer (not in persistence) -```java -// Domain Model -public class Item { - public static Item from(CreateItemCommand command) { - return Item.builder() - .itemName(command.getItemName()) - .itemCode(command.getItemCode()) - .isEnabled(command.getIsEnabled()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - } -} - -// Usage in Service -public class ItemService { - public void createItem(CreateItemCommand command) { - Item item = Item.from(command); // ← Static factory - itemCommandRepository.save(item); - } -} -``` - -**Common factory method names:** -- `from()`: Create from another object (Command, DTO, etc.) -- `of()`: Create from primitive values -- `create()`: Create with default values +**Examples:** See `examples.md#static-factory-methods`. --- # Audit Fields Management (MUST) -## Persistence Layer (automatic via BaseTimeEntity) +## Persistence Layer -```java -@MappedSuperclass -@EntityListeners(AuditingEntityListener.class) -public abstract class BaseTimeEntity { - @CreatedDate - @Column(updatable = false) - private LocalDateTime createdAt; - - @LastModifiedDate - private LocalDateTime updatedAt; -} -``` +- Use `BaseTimeEntity` for auto-managed timestamps -All entities extend this: -```java -@Entity -public class ItemEntity extends BaseTimeEntity { // ← Auto-managed timestamps - // ... -} -``` +## Domain Layer -## Domain Layer (manual) +- Manage createdAt/updatedAt/deletedAt manually -```java -@Getter -@Builder(toBuilder = true) -public class Item { - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - private LocalDateTime deletedAt; // ← For soft delete - - public static Item from(CreateItemCommand command) { - return Item.builder() - // ... - .createdAt(LocalDateTime.now()) // ← Manual - .updatedAt(LocalDateTime.now()) // ← Manual - .build(); - } - - public void update(UpdateItemCommand command) { - // ... - this.updatedAt = LocalDateTime.now(); // ← Manual update - } - - public void softDelete() { - this.deletedAt = LocalDateTime.now(); // ← Soft delete - } -} -``` +**Examples:** See `examples.md#audit-fields-management`. --- @@ -588,165 +277,66 @@ public class Item { ## DO (✅) -- ✅ Keep domain models free of JPA annotations -- ✅ Use IDs to reference other entities in domain models -- ✅ Define relationships (@ManyToOne, etc.) in persistence entities only -- ✅ Use ModelMapper for Domain ↔ Entity conversion -- ✅ Put business logic in domain models -- ✅ Use static factory methods (from, of) for domain creation -- ✅ Extend BaseTimeEntity for all persistence entities -- ✅ Use protected/private constructors with Builder pattern -- ✅ Use BINARY(16) for all UUID columns -- ✅ Use BigDecimal for weights, rates, and decimal values -- ✅ Use BigInteger for monetary amounts -- ✅ Use BigDecimal.ZERO and compareTo() for BigDecimal operations +- Keep domain models pure POJOs +- Use ID-only references in domain models +- Use ModelMapper in repositories for conversion +- Use BINARY(16) for UUID columns +- Use BigDecimal/BigInteger per type rules ## DON'T (❌) -- ❌ Don't add JPA annotations to domain models -- ❌ Don't reference other domain objects directly (use IDs) -- ❌ Don't put business logic in persistence entities -- ❌ Don't expose public no-args constructors on entities -- ❌ Don't use @Embeddable in domain VOs (only in persistence VOs) -- ❌ Don't create bidirectional relationships without good reason -- ❌ Don't use FetchType.EAGER (always prefer LAZY) -- ❌ Don't store UUID as VARCHAR or CHAR(36) - always use BINARY(16) -- ❌ Don't use Float or Double for monetary or precision-critical values -- ❌ Don't use equals() for BigDecimal comparison - use compareTo() +- Put JPA annotations in domain models +- Use object references in domain models +- Put business logic in entities +- Use VARCHAR/CHAR for UUID +- Use BigDecimal equals() for comparisons + +**Code samples:** See `examples.md#common-pattern-examples`. --- # Common Anti-Patterns (MUST AVOID) -## ❌ Anti-Pattern 1: JPA Annotations in Domain Model +1. JPA annotations in domain models +2. Direct object references in domain models +3. Business logic in entities +4. Using VARCHAR/CHAR for UUID +5. Using `equals()` for BigDecimal comparisons -```java -// ❌ WRONG: Domain model with JPA annotations -@Entity // ← NO! -@Table(name = "item") // ← NO! -@Getter -@Builder -public class Item { - @Id // ← NO! - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "item_name") // ← NO! - private String itemName; -} -``` - -**✅ CORRECT:** Keep domain model as pure POJO (shown above). - -## ❌ Anti-Pattern 2: Direct Object References in Domain - -```java -// ❌ WRONG: Domain model with object references -@Getter -@Builder -public class TradeItem { - private Long id; - private Trade trade; // ← NO! Use tradeId instead - private Item item; // ← NO! Use itemId instead -} -``` - -**✅ CORRECT:** -```java -@Getter -@Builder -public class TradeItem { - private Long id; - private Long tradeId; // ← Use ID - private Long itemId; // ← Use ID -} -``` - -## ❌ Anti-Pattern 3: Business Logic in Entity - -```java -// ❌ WRONG: Business logic in persistence entity -@Entity -@Table(name = "item") -public class ItemEntity extends BaseTimeEntity { - // ... - - public void validateEnabled() { // ← NO business logic in Entity! - if (!this.isEnabled) { - throw new BaseException(BaseResponseStatus.ITEM_DISABLED); - } - } -} -``` - -**✅ CORRECT:** Put business logic in Domain Model (shown above). - -## ❌ Anti-Pattern 4: Using VARCHAR for UUID - -```java -// ❌ WRONG -@Column(name = "user_uuid", columnDefinition = "VARCHAR(36)") -private UUID userUuid; - -// ❌ ALSO WRONG -@Column(name = "user_uuid", columnDefinition = "CHAR(36)") -private UUID userUuid; -``` - -**✅ CORRECT:** -```java -@Column(name = "user_uuid", columnDefinition = "BINARY(16)", nullable = false, unique = true) -private UUID userUuid; -``` - -## ❌ Anti-Pattern 5: Using equals() for BigDecimal - -```java -// ❌ WRONG -if (amount.equals(BigDecimal.ZERO)) { ... } -if (amount1.equals(amount2)) { ... } -``` - -**✅ CORRECT:** -```java -if (amount.compareTo(BigDecimal.ZERO) == 0) { ... } -if (amount1.compareTo(amount2) == 0) { ... } -``` +**Code samples:** See `examples.md#anti-patterns`. --- # Summary Checklist -When implementing or reviewing domain/persistence code, ensure: - ## Domain Model: - [ ] Pure POJO (no JPA annotations) -- [ ] References other entities by ID only -- [ ] Contains business logic methods -- [ ] Uses static factory methods (from, of) -- [ ] Manual timestamp management +- [ ] ID-only references (no object references) +- [ ] Business logic exists in domain +- [ ] Static factory method exists +- [ ] Manual timestamps in domain ## Persistence Entity: +- [ ] Has JPA annotations (@Entity, @Table, @Column) - [ ] Extends BaseTimeEntity -- [ ] Has @Entity, @Table, @Column annotations -- [ ] Protected no-args + private all-args constructors -- [ ] NO business logic -- [ ] Relationships use FetchType.LAZY +- [ ] No business logic +- [ ] Relationships in entities only +- [ ] Uses FetchType.LAZY ## Value Objects: -- [ ] Domain VO: no @Embeddable -- [ ] Persistence VO: @Embeddable with @Column +- [ ] Domain VO has no @Embeddable +- [ ] Persistence VO uses @Embeddable +- [ ] Proper constructors & @Column annotations ## Repository: -- [ ] Uses ModelMapper for Domain ↔ Entity conversion -- [ ] Command repo for saves, Query repo for reads +- [ ] ModelMapper used for conversion +- [ ] Explicit conversion steps +- [ ] Throws BaseException when not found ## Types: - [ ] UUID uses BINARY(16) -- [ ] BigDecimal for decimals (weights, rates, carbon) -- [ ] BigInteger for monetary amounts -- [ ] Integer for counts -- [ ] BigDecimal.ZERO and compareTo() usage +- [ ] BigDecimal uses precision/scale +- [ ] BigInteger for money ## If ANY checklist item is unchecked, the implementation violates architecture rules. @@ -754,275 +344,37 @@ When implementing or reviewing domain/persistence code, ensure: # Output Contract -When domain/persistence implementation completes, you MUST return: - ## 1. Implementation Summary -**Format:** -```markdown -## Domain & Persistence Implementation Summary - -**Domain Model:** [Name] (pure POJO) -**Persistence Entity:** [Name]Entity (JPA) -**Value Objects:** [count] ([list names]) -**Repository:** [Name]CommandRepository, [Name]QueryRepository - -**Files created/modified:** -- Domain Model: [domain]/domain/model/[Name].java -- Persistence Entity: [domain]/adapter/out/persistence/entity/[Name]Entity.java -- Domain VO: [domain]/domain/model/vo/[VO] (if applicable) -- Persistence VO: [domain]/adapter/out/persistence/entity/vo/[VO]Vo (if applicable) -- Repositories: [domain]/adapter/out/persistence/repository/[Name]*Repository*.java -``` +Provide: +- Domain Model name +- Persistence Entity name +- VO usage +- Repository types (Command/Query) ## 2. Verification Results -**Checklist confirmation:** -```markdown -## Architecture Compliance Verification - -### Domain Model ([Name].java): -✅ Pure POJO (NO @Entity, @Table, @Column) -✅ Uses IDs for references (e.g., Long itemTypeId, NOT ItemType itemType) -✅ Contains business logic methods -✅ Uses static factory method (from/of/create) -✅ Manual timestamp management (createdAt, updatedAt) - -### Persistence Entity ([Name]Entity.java): -✅ Extends BaseTimeEntity -✅ Has @Entity, @Table, @Column annotations -✅ Protected no-args + private all-args constructors -✅ NO business logic -✅ Relationships use FetchType.LAZY -✅ Helper methods for ID extraction (if needed) - -### Type Handling: -✅ UUID uses BINARY(16) in @Column -✅ BigDecimal for weights/rates (with precision/scale) -✅ BigInteger for monetary amounts -✅ BigDecimal.ZERO and compareTo() usage - -### Value Objects (if applicable): -✅ Domain VO: no @Embeddable annotation -✅ Persistence VO: @Embeddable with @Column annotations - -### Repository Pattern: -✅ ModelMapper for Domain ↔ Entity conversion -✅ Command repo for saves, Query repo for reads -✅ Throws BaseException when entity not found -``` +Provide: +- Domain model compliance checks +- Persistence entity compliance checks +- Type handling checks +- Repository conversion checks ## 3. File Locations -**Exact paths:** -```markdown -### Domain Layer: -`src/main/java/greenfirst/be/[domain]/domain/model/[Name].java` -`src/main/java/greenfirst/be/[domain]/domain/model/vo/[VO].java` (if VO exists) - -### Persistence Layer: -`src/main/java/greenfirst/be/[domain]/adapter/out/persistence/entity/[Name]Entity.java` -`src/main/java/greenfirst/be/[domain]/adapter/out/persistence/entity/vo/[VO]Vo.java` (if VO exists) - -### Repository Layer: -`src/main/java/greenfirst/be/[domain]/application/port/out/[Name]CommandRepository.java` (interface) -`src/main/java/greenfirst/be/[domain]/application/port/out/[Name]QueryRepository.java` (interface) -`src/main/java/greenfirst/be/[domain]/adapter/out/persistence/repository/[Name]CommandRepositoryImpl.java` -`src/main/java/greenfirst/be/[domain]/adapter/out/persistence/repository/[Name]QueryRepositoryImpl.java` -``` +Provide file paths for: +- Domain model +- Persistence entity +- VOs (if any) +- Repositories ## 4. Common Pattern Examples -**Show key implementations:** -```markdown -### Domain Model Pattern: -```java -@Getter -@NoArgsConstructor -@Builder(toBuilder = true) -public class [Name] { - private Long id; - private String [field]; - private Long [relatedId]; // ✅ ID only, not object - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - public static [Name] from([Command] command) { - return [Name].builder() - .[field](command.get[Field]()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - } - - public void update([Command] command) { - this.[field] = command.get[Field](); - this.updatedAt = LocalDateTime.now(); - } -} -``` - -### Persistence Entity Pattern: -```java -@Entity -@Table(name = "[table_name]") -@Getter -@Builder(toBuilder = true) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class [Name]Entity extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "[id_column]") - private Long id; - - @Column(name = "[field_column]", length = 100) - private String [field]; - - @Column(name = "uuid_field", columnDefinition = "BINARY(16)") - private UUID uuidField; // ✅ BINARY(16) for UUID - - @Column(name = "amount", precision = 10, scale = 2) - private BigDecimal amount; // ✅ BigDecimal with precision -} -``` - -### Repository Conversion Pattern: -```java -@Override -public [Name] save([Name] domain) { - // Domain → Entity - [Name]Entity entity = modelMapper.map(domain, [Name]Entity.class); - - // Persist - [Name]Entity saved = jpaRepository.save(entity); - - // Entity → Domain - return modelMapper.map(saved, [Name].class); -} - -@Override -public [Name] findById(Long id) { - [Name]Entity entity = jpaRepository.findById(id) - .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_EXIST_[NAME])); - - return modelMapper.map(entity, [Name].class); -} -``` -``` +See `examples.md#common-pattern-examples`. ## Output Example -```markdown -## Domain & Persistence Implementation Summary - -**Domain Model:** Trade (pure POJO) -**Persistence Entity:** TradeEntity (JPA) -**Value Objects:** 0 -**Repository:** TradeCommandRepository, TradeQueryRepository - -**Files created/modified:** -- Domain Model: trade/domain/model/Trade.java -- Persistence Entity: trade/adapter/out/persistence/entity/TradeEntity.java -- Repositories: trade/adapter/out/persistence/repository/Trade*Repository*.java - ---- - -## Architecture Compliance Verification - -### Domain Model (Trade.java): -✅ Pure POJO (NO @Entity, @Table, @Column) -✅ Uses IDs for references (Long itemId, UUID sellerUuid) -✅ Contains business logic methods (validateSearchAccess, calculateTotalAmount) -✅ Uses static factory method (from) -✅ Manual timestamp management (createdAt, updatedAt) - -### Persistence Entity (TradeEntity.java): -✅ Extends BaseTimeEntity -✅ Has @Entity, @Table, @Column annotations -✅ Protected no-args + private all-args constructors -✅ NO business logic -✅ Relationships use FetchType.LAZY (TradeItemEntity) - -### Type Handling: -✅ UUID uses BINARY(16): sellerUuid, buyerUuid -✅ BigDecimal for weights: totalWeight (precision=10, scale=2) -✅ BigInteger for amounts: tradeAmount -✅ BigDecimal.ZERO and compareTo() used in calculations - -### Repository Pattern: -✅ ModelMapper for Domain ↔ Entity conversion -✅ TradeCommandRepositoryImpl for saves -✅ TradeQueryRepositoryImpl for reads -✅ Throws BaseException(NO_EXIST_TRADE) when not found - ---- - -### File Locations: - -**Domain Layer:** -`src/main/java/greenfirst/be/trade/domain/model/Trade.java` - -**Persistence Layer:** -`src/main/java/greenfirst/be/trade/adapter/out/persistence/entity/TradeEntity.java` - -**Repository Layer:** -`src/main/java/greenfirst/be/trade/application/port/out/TradeCommandRepository.java` (interface) -`src/main/java/greenfirst/be/trade/application/port/out/TradeQueryRepository.java` (interface) -`src/main/java/greenfirst/be/trade/adapter/out/persistence/repository/TradeCommandRepositoryImpl.java` -`src/main/java/greenfirst/be/trade/adapter/out/persistence/repository/TradeQueryRepositoryImpl.java` - ---- - -### Key Implementation Snippets: - -**Domain Model - ID References:** -```java -@Getter -@Builder(toBuilder = true) -public class Trade { - private Long id; - private UUID sellerUuid; // ✅ UUID, not Users object - private UUID buyerUuid; // ✅ UUID, not Users object - private BigInteger tradeAmount; // ✅ BigInteger for money - private BigDecimal totalWeight; // ✅ BigDecimal for weight - - public static Trade from(CreateTradeCommand command) { - return Trade.builder() - .sellerUuid(command.getSellerUuid()) - .buyerUuid(command.getBuyerUuid()) - .tradeAmount(command.getTradeAmount()) - .createdAt(LocalDateTime.now()) - .build(); - } -} -``` - -**Persistence Entity - BINARY(16) for UUID:** -```java -@Entity -@Table(name = "trade") -public class TradeEntity extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "seller_uuid", columnDefinition = "BINARY(16)", nullable = false) - private UUID sellerUuid; // ✅ BINARY(16) - - @Column(name = "buyer_uuid", columnDefinition = "BINARY(16)", nullable = false) - private UUID buyerUuid; // ✅ BINARY(16) - - @Column(name = "trade_amount", nullable = false) - private BigInteger tradeAmount; - - @Column(name = "total_weight", precision = 10, scale = 2) - private BigDecimal totalWeight; // ✅ Precision specified -} -``` - -✅ Implementation complete and verified. -``` +See `examples.md#output-example`. --- diff --git a/.claude/skills/domain-persistence/examples.md b/.claude/skills/domain-persistence/examples.md new file mode 100644 index 0000000..c12fbc1 --- /dev/null +++ b/.claude/skills/domain-persistence/examples.md @@ -0,0 +1,545 @@ +# Domain-Persistence Examples + +## Domain Model Example + +```java +@Getter +@NoArgsConstructor +@Builder(toBuilder = true) +public class Items { + private Long id; + private String itemName; + private String itemCode; + private Boolean isEnabled; + private Integer itemTypeId; // ← ID reference, not object + private Integer unitPrice; + private String unit; + private Long carbonEmissionFactorId; // ← ID reference, not object + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + // ✅ Static factory method + public static Items from(CreateItemCommand command) { + return Items.builder() + .itemName(command.getItemName()) + .itemCode(command.getItemCode()) + .isEnabled(command.getIsEnabled()) + .itemTypeId(command.getItemTypeId()) + .unitPrice(command.getUnitPrice()) + .unit(command.getUnit()) + .carbonEmissionFactorId(command.getCarbonEmissionFactorId()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + } + + // ✅ Business logic method + public void updateItemInfo(UpdateItemCommand command) { + this.itemName = command.getItemName(); + this.itemCode = command.getItemCode(); + this.isEnabled = command.getIsEnabled(); + this.updatedAt = LocalDateTime.now(); + } + + // ✅ Validation method + public void validateEnabled() { + if (!this.isEnabled) { + throw new BaseException(BaseResponseStatus.ITEM_DISABLED); + } + } +} +``` + +## Persistence Entity Example + +```java +@Entity +@Table(name = "item") +@Getter +@Builder(toBuilder = true) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ItemEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "item_id") + private Long id; + + @Column(name = "item_name", length = 100, nullable = false) + private String itemName; + + @Column(name = "item_code", length = 50, unique = true, nullable = false) + private String itemCode; + + @Column(name = "is_enabled", nullable = false) + private Boolean isEnabled; + + @Column(name = "item_type_id") + private Integer itemTypeId; + + @Column(name = "unit_price") + private Integer unitPrice; + + @Column(name = "unit", length = 20) + private String unit; + + @Column(name = "carbon_emission_factor_id") + private Long carbonEmissionFactorId; +} +``` + +## Value Objects + +### Domain Value Object + +```java +@Getter +@NoArgsConstructor +@Builder +public class BranchOption { + @NotNull + private Long branchId; + + @NotBlank + private String branchName; +} +``` + +### Persistence Value Object + +```java +@Getter +@Builder +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class BranchOptionVo { + @Column(name = "branch_id") + private Long branchId; + + @Column(name = "branch_name", length = 100) + private String branchName; +} +``` + +### Usage in Entity + +```java +@Entity +@Table(name = "users") +public class UserEntity extends BaseTimeEntity { + @Embedded + private BranchOptionVo branchOption; // ← Use Persistence VO +} +``` + +## Relationship Mapping + +### Domain Model (ID-only) + +```java +@Getter +@Builder +public class TradeItems { + private Long id; + private Long tradeId; // ← Just the ID + private Long itemId; // ← Just the ID + private Integer quantity; +} +``` + +### Persistence Entity (relationship) + +```java +@Entity +@Table(name = "trade_item") +public class TradeItemEntity extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) // ← Relationship only in Entity + @JoinColumn(name = "trade_id", nullable = false) + private TradeEntity trade; + + @Column(name = "item_id") + private Long itemId; + + @Column(name = "quantity") + private Integer quantity; + + // Helper method to extract ID for domain conversion + public Long getTradeId() { + return trade != null ? trade.getId() : null; + } +} +``` + +## Repository Conversion Pattern + +### Command Repository (save operations) + +```java +@Repository +@RequiredArgsConstructor +public class ItemCommandRepositoryImpl implements ItemCommandRepository { + private final ItemJpaRepository itemJpaRepository; + private final ModelMapper modelMapper; + + @Override + public Items save(Items items) { + // Domain → Entity + ItemEntity itemEntity = modelMapper.map(items, ItemEntity.class); + + // Persist + ItemEntity savedEntity = itemJpaRepository.save(itemEntity); + + // Entity → Domain + return modelMapper.map(savedEntity, Items.class); + } +} +``` + +### Query Repository (read operations) + +```java +@Repository +@RequiredArgsConstructor +public class ItemQueryRepositoryImpl implements ItemQueryRepository { + private final ItemJpaRepository itemJpaRepository; + private final ModelMapper modelMapper; + + @Override + public Items findById(Long id) { + ItemEntity entity = itemJpaRepository.findById(id) + .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_EXIST_ITEM)); + + // Entity → Domain + return modelMapper.map(entity, Items.class); + } + + @Override + public List findAll() { + return itemJpaRepository.findAll().stream() + .map(entity -> modelMapper.map(entity, Items.class)) + .toList(); + } +} +``` + +## Static Factory Methods + +```java +// Domain Model +public class Items { + public static Items from(CreateItemCommand command) { + return Items.builder() + .itemName(command.getItemName()) + .itemCode(command.getItemCode()) + .isEnabled(command.getIsEnabled()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + } +} + +// Usage in Service +public class ItemService { + public void createItem(CreateItemCommand command) { + Items items = Items.from(command); // ← Static factory + itemCommandRepository.save(items); + } +} +``` + +## Audit Fields Management + +### Persistence Layer (automatic via BaseTimeEntity) + +```java +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} +``` + +All entities extend this: +```java +@Entity +public class ItemEntity extends BaseTimeEntity { // ← Auto-managed timestamps + // ... +} +``` + +### Domain Layer (manual) + +```java +@Getter +@Builder(toBuilder = true) +public class Items { + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private LocalDateTime deletedAt; // ← For soft delete + + public static Items from(CreateItemCommand command) { + return Items.builder() + // ... + .createdAt(LocalDateTime.now()) // ← Manual + .updatedAt(LocalDateTime.now()) // ← Manual + .build(); + } + + public void update(UpdateItemCommand command) { + // ... + this.updatedAt = LocalDateTime.now(); // ← Manual update + } + + public void softDelete() { + this.deletedAt = LocalDateTime.now(); // ← Soft delete + } +} +``` + +## Common Pattern Examples + +````markdown +### Domain Model Pattern: +```java +@Getter +@NoArgsConstructor +@Builder(toBuilder = true) +public class [Name] { + private Long id; + private String [field]; + private Long [relatedId]; // ✅ ID only, not object + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static [Name] from([Command] command) { + return [Name].builder() + .[field](command.get[Field]()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + } + + public void update([Command] command) { + this.[field] = command.get[Field](); + this.updatedAt = LocalDateTime.now(); + } +} +``` + +### Persistence Entity Pattern: +```java +@Entity +@Table(name = "[table_name]") +@Getter +@Builder(toBuilder = true) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class [Name]Entity extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "[id_column]") + private Long id; + + @Column(name = "[field_column]", length = 100) + private String [field]; + + @Column(name = "uuid_field", columnDefinition = "BINARY(16)") + private UUID uuidField; // ✅ BINARY(16) for UUID + + @Column(name = "amount", precision = 10, scale = 2) + private BigDecimal amount; // ✅ BigDecimal with precision +} +``` + +### Repository Conversion Pattern: +```java +@Override +public [Name] save([Name] domain) { + // Domain → Entity + [Name]Entity entity = modelMapper.map(domain, [Name]Entity.class); + + // Persist + [Name]Entity saved = jpaRepository.save(entity); + + // Entity → Domain + return modelMapper.map(saved, [Name].class); +} + +@Override +public [Name] findById(Long id) { + [Name]Entity entity = jpaRepository.findById(id) + .orElseThrow(() -> new BaseException(BaseResponseStatus.NO_EXIST_[NAME])); + + return modelMapper.map(entity, [Name].class); +} +``` +```` + +## Anti-Patterns + +### ❌ JPA Annotations in Domain Model + +```java +@Entity // ❌ Wrong place +public class Items { + // ... +} +``` + +### ❌ Direct Object References in Domain + +```java +public class Items { + private ItemType itemType; // ❌ Object reference +} +``` + +### ❌ Business Logic in Entity + +```java +@Entity +public class ItemEntity { + public void updatePrice(Integer price) { // ❌ Business logic in entity + // ... + } +} +``` + +### ❌ Using VARCHAR for UUID + +```java +@Column(name = "user_uuid", columnDefinition = "VARCHAR(36)") // ❌ Wrong +private UUID userUuid; +``` + +### ❌ Using equals() for BigDecimal + +```java +if (amount.equals(BigDecimal.ZERO)) { // ❌ Wrong + // ... +} +``` + +## Output Example + +````markdown +## Domain & Persistence Implementation Summary + +**Domain Model:** Trades (pure POJO) +**Persistence Entity:** TradeEntity (JPA) +**Value Objects:** 0 +**Repository:** TradeCommandRepository, TradeQueryRepository + +**Files created/modified:** +- Domain Model: trade/domain/model/Trades.java +- Persistence Entity: trade/adapter/out/persistence/entity/TradeEntity.java +- Repositories: trade/adapter/out/persistence/repository/Trade*Repository*.java + +--- + +## Architecture Compliance Verification + +### Domain Model (Trades.java): +✅ Pure POJO (NO @Entity, @Table, @Column) +✅ Uses IDs for references (Long itemId, UUID sellerUuid) +✅ Contains business logic methods (validateSearchAccess, calculateTotalAmount) +✅ Uses static factory method (from) +✅ Manual timestamp management (createdAt, updatedAt) + +### Persistence Entity (TradeEntity.java): +✅ Extends BaseTimeEntity +✅ Has @Entity, @Table, @Column annotations +✅ Protected no-args + private all-args constructors +✅ NO business logic +✅ Relationships use FetchType.LAZY (TradeItemEntity) + +### Type Handling: +✅ UUID uses BINARY(16): sellerUuid, buyerUuid +✅ BigDecimal for weights: totalWeight (precision=10, scale=2) +✅ BigInteger for amounts: tradeAmount +✅ BigDecimal.ZERO and compareTo() used in calculations + +### Repository Pattern: +✅ ModelMapper for Domain ↔ Entity conversion +✅ TradeCommandRepositoryImpl for saves +✅ TradeQueryRepositoryImpl for reads +✅ Throws BaseException(NO_EXIST_TRADE) when not found + +--- + +### File Locations: + +**Domain Layer:** +`src/main/java/greenfirst/be/trade/domain/model/Trades.java` + +**Persistence Layer:** +`src/main/java/greenfirst/be/trade/adapter/out/persistence/entity/TradeEntity.java` + +**Repository Layer:** +`src/main/java/greenfirst/be/trade/application/port/out/TradeCommandRepository.java` (interface) +`src/main/java/greenfirst/be/trade/application/port/out/TradeQueryRepository.java` (interface) +`src/main/java/greenfirst/be/trade/adapter/out/persistence/repository/TradeCommandRepositoryImpl.java` +`src/main/java/greenfirst/be/trade/adapter/out/persistence/repository/TradeQueryRepositoryImpl.java` + +--- + +### Key Implementation Snippets: + +**Domain Model - ID References:** +```java +@Getter +@Builder(toBuilder = true) +public class Trades { + private Long id; + private UUID sellerUuid; // ✅ UUID, not Users object + private UUID buyerUuid; // ✅ UUID, not Users object + private BigInteger tradeAmount; // ✅ BigInteger for money + private BigDecimal totalWeight; // ✅ BigDecimal for weight + + public static Trades from(CreateTradeCommand command) { + return Trades.builder() + .sellerUuid(command.getSellerUuid()) + .buyerUuid(command.getBuyerUuid()) + .tradeAmount(command.getTradeAmount()) + .createdAt(LocalDateTime.now()) + .build(); + } +} +``` + +**Persistence Entity - BINARY(16) for UUID:** +```java +@Entity +@Table(name = "trade") +public class TradeEntity extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "seller_uuid", columnDefinition = "BINARY(16)", nullable = false) + private UUID sellerUuid; // ✅ BINARY(16) + + @Column(name = "buyer_uuid", columnDefinition = "BINARY(16)", nullable = false) + private UUID buyerUuid; // ✅ BINARY(16) + + @Column(name = "trade_amount", nullable = false) + private BigInteger tradeAmount; + + @Column(name = "total_weight", precision = 10, scale = 2) + private BigDecimal totalWeight; // ✅ Precision specified +} +``` + +✅ Implementation complete and verified. +```` diff --git a/.claude/skills/domain-persistence/reference.md b/.claude/skills/domain-persistence/reference.md new file mode 100644 index 0000000..9f2c7c0 --- /dev/null +++ b/.claude/skills/domain-persistence/reference.md @@ -0,0 +1,128 @@ +# Domain-Persistence Reference + +## UUID and Numeric Type Handling + +### UUID Fields (MUST use BINARY(16)) + +All UUID fields in persistence entities MUST use `BINARY(16)` column definition: + +**Persistence Entity:** +```java +@Entity +@Table(name = "users") +public class UserEntity extends BaseTimeEntity { + @Column(name = "user_uuid", nullable = false, columnDefinition = "BINARY(16)", unique = true) + private UUID userUuid; +} + +@Entity +@Table(name = "trade") +public class TradeEntity extends BaseTimeEntity { + @Column(name = "seller_uuid", columnDefinition = "BINARY(16)", nullable = false) + private UUID sellerUuid; + + @Column(name = "buyer_uuid", columnDefinition = "BINARY(16)", nullable = false) + private UUID buyerUuid; +} +``` + +**Domain Model:** +```java +@Getter +@Builder(toBuilder = true) +public class Users { + private UUID userUuid; // ← Plain UUID, no annotations +} + +@Getter +@Builder(toBuilder = true) +public class Trades { + private UUID sellerUuid; + private UUID buyerUuid; +} +``` + +### BigDecimal Usage (for decimals) + +Use `BigDecimal` for: +- **Weights** (무게): `totalWeight`, `payloadWeight`, `curbWeight` +- **Rates/Ratios** (비율): `totalWeightLossRate`, `emissionFactorKgCo2ePerKg` +- **Carbon values** (탄소 관련): `carbonReductionAmount` +- **Prices with decimals** (소수점 가격): `userTradeItemUnitPrice` + +**Entity:** +```java +@Entity +@Table(name = "trade") +public class TradeEntity extends BaseTimeEntity { + @Column(name = "total_weight", precision = 10, scale = 2) + private BigDecimal totalWeight; + + @Column(name = "total_weight_loss_rate", precision = 5, scale = 2) + private BigDecimal totalWeightLossRate; + + @Column(name = "carbon_reduction_amount", precision = 10, scale = 2) + private BigDecimal carbonReductionAmount; +} +``` + +**Domain Model:** +```java +@Getter +@Builder(toBuilder = true) +public class Trades { + private BigDecimal totalWeight; + private BigDecimal totalWeightLoss; + private BigDecimal totalWeightLossRate; + private BigDecimal carbonReductionAmount; + + // ✅ Business logic using BigDecimal + public BigDecimal calculateWeightLoss() { + if (totalWeight != null && payloadWeight != null) { + return totalWeight.subtract(payloadWeight); + } + return BigDecimal.ZERO; // ← Use ZERO constant + } +} +``` + +### BigInteger Usage (for large integers) + +Use `BigInteger` for: +- **Monetary amounts** (금액): `tradeAmount`, `totalBuyAmount`, `totalSellAmount` +- **Large integer values** without decimal points + +```java +@Entity +@Table(name = "trade") +public class TradeEntity extends BaseTimeEntity { + @Column(name = "trade_amount", nullable = false) + private BigInteger tradeAmount; // ← 금액은 BigInteger +} +``` + +### Integer Usage (for counts/small values) + +Use `Integer` for: +- **Counts**: `totalBuyCount`, `totalSellCount` +- **Unit prices** when stored as integers: `unitPrice` +- **Small numeric IDs or codes** + +### BigDecimal Guidelines (MUST) + +- ✅ Use `BigDecimal.ZERO` instead of `new BigDecimal(0)` for zero values +- ✅ Always use `compareTo()` for BigDecimal comparisons, **NOT `equals()`** +- ✅ Specify `precision` and `scale` in @Column for BigDecimal fields +- ✅ Handle null checks before BigDecimal arithmetic operations + +**Example:** +```java +// ❌ WRONG +if (amount.equals(BigDecimal.ZERO)) { ... } + +// ✅ CORRECT +if (amount.compareTo(BigDecimal.ZERO) == 0) { ... } + +// ✅ CORRECT +if (amount1.compareTo(amount2) > 0) { ... } // amount1 > amount2 +``` diff --git a/.claude/skills/test-code/SKILL.md b/.claude/skills/test-code/SKILL.md index 125b716..0b30595 100644 --- a/.claude/skills/test-code/SKILL.md +++ b/.claude/skills/test-code/SKILL.md @@ -34,7 +34,7 @@ Activate this skill when: ## The Problem -**2026년 1월 Production Data Loss Incident:** +**2026년 1월경 Production Data Loss Incident:** - E2E test used `deleteAllInBatch()` in `@AfterEach` - **ALL production/development data was deleted** - Data was unrecoverable diff --git a/.claude/skills/web-layer/SKILL.md b/.claude/skills/web-layer/SKILL.md index 5ce06e6..fc446c6 100644 --- a/.claude/skills/web-layer/SKILL.md +++ b/.claude/skills/web-layer/SKILL.md @@ -176,16 +176,24 @@ public class IncentiveResponse { ## When to Use ModelMapper (Optional) -For simple 1:1 field mappings, you can use ModelMapper: +For simple 1:1 field mappings, you can use ModelMapper, but **only via a shared bean**: ```java -public static TradeDetailResponse from(TradeDetailOutDto outDto) { - ModelMapper modelMapper = new ModelMapper(); - return modelMapper.map(outDto, TradeDetailResponse.class); +@Component +@RequiredArgsConstructor +public class TradeResponseMapper { + private final ModelMapper modelMapper; + + public TradeDetailResponse toResponse(TradeDetailOutDto outDto) { + return modelMapper.map(outDto, TradeDetailResponse.class); + } } ``` -But explicit mapping is preferred for: +Do NOT instantiate `new ModelMapper()` inside conversion methods. +If you use ModelMapper, keep it outside controllers (e.g., mapper/service) so controllers stay free of conversion logic. + +Explicit mapping is preferred for: - Complex transformations - Nested objects - Field name differences @@ -478,7 +486,7 @@ When web layer implementation completes, you MUST return: ## 4. Code Examples **Show key implementations:** -```markdown +````markdown ### Controller Method Example: ```java @GetMapping("/{id}") @@ -501,7 +509,7 @@ public static [Response] from([OutDto] outDto) { .build(); } ``` -``` +```` ## Output Example diff --git a/CLAUDE.md b/CLAUDE.md index 049e6b4..2e0960a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **E2E 테스트 작성 시 `deleteAllInBatch()` 절대 사용 금지!** -- 2026년 1월: E2E 테스트의 `@AfterEach`에서 `deleteAllInBatch()` 사용으로 **실제 운영 DB 데이터 전체 삭제 사고 발생** +- 2026년 1월경: E2E 테스트의 `@AfterEach`에서 `deleteAllInBatch()` 사용으로 **실제 운영 DB 데이터 전체 삭제 사고 발생** - E2E 테스트는 실제 DB를 사용하므로, 테스트에서 **생성한 데이터 ID만 추적하여 삭제**해야 함 - 자세한 내용은 **`test-code` SKILL** 참조 - **테스트 프로필은 반드시 분리된 DB만 사용** (기본: `greenfirst_test`) @@ -105,7 +105,7 @@ src/main/java/greenfirst/be/ ## Layer-Specific Implementation -Detailed implementation patterns for each layer are documented in specialized skills. Use these skills for specific implementation tasks: +Detailed implementation patterns for each layer are documented in specialized skills. Use these skills for specific implementation tasks (see each skill’s `SKILL.md` for specifics): ### Web Layer (Controller) @@ -152,7 +152,7 @@ public BaseResponse get(@PathVariable Long id) { - ✅ ModelMapper로 Domain ↔ Entity 변환 **Naming:** -- Domain Model: `Users`, `Item`, `Trade` (pure POJO) +- Domain Model: `Users`, `Items`, `Trades` (pure POJO) - Persistence Entity: `UserEntity`, `ItemEntity`, `TradeEntity` (JPA) - Domain VO: `BranchOption` (no @Embeddable) - Persistence VO: `BranchOptionVo` (@Embeddable) @@ -164,11 +164,11 @@ public BaseResponse get(@PathVariable Long id) { // Domain Model (NO JPA) @Getter @Builder(toBuilder = true) -public class Item { +public class Items { private Long id; private Integer itemTypeId; // ← ID only, not ItemType object - public static Item from(CreateItemCommand command) { ... } + public static Items from(CreateItemCommand command) { ... } public void update(UpdateItemCommand command) { ... } // Business logic }