Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a4c6288
feat: implement seller sign-up with layered architecture
dnwls16071 Jul 12, 2026
0e48b13
docs: document key seller sign-up specs with KDoc
dnwls16071 Jul 12, 2026
becaab3
test: use BCrypt encoder in test config
dnwls16071 Jul 12, 2026
2aa6f27
docs: add seller access token issuance test scenarios
dnwls16071 Jul 13, 2026
c9c9dda
refactor: convert sign-up controller from record to class
dnwls16071 Jul 13, 2026
3211bb4
feat: implement seller access token issuance API
dnwls16071 Jul 13, 2026
b15b430
test: introduce @TddApiTest composed annotation for API spec bootstrap
dnwls16071 Jul 13, 2026
665ff25
docs: add shopper sign-up policy and test scenarios
dnwls16071 Jul 13, 2026
27f5879
feat: implement shopper sign-up API
dnwls16071 Jul 13, 2026
37f2952
refactor: centralize exception handling with BusinessException and Er…
dnwls16071 Jul 14, 2026
f42dc4e
style: apply Naver Java convention formatting
dnwls16071 Jul 14, 2026
9adf899
refactor: abstract security components behind application-layer ports
dnwls16071 Jul 14, 2026
051e647
docs: add seller profile query API test scenarios
dnwls16071 Jul 14, 2026
0fb3fcb
feat: implement seller profile query API
dnwls16071 Jul 14, 2026
35104b7
refactor: route seller profile lookup through an application service
dnwls16071 Jul 14, 2026
9c6e817
docs: add shopper profile query API test scenarios
dnwls16071 Jul 14, 2026
17837ef
feat: implement shopper token issuance and profile query API
dnwls16071 Jul 14, 2026
b12fbc7
refactor: extract TestFixture to remove duplication in me specs
dnwls16071 Jul 14, 2026
d1f1a48
test: introduce declarative invalid-value source annotations
dnwls16071 Jul 14, 2026
2fdf4fc
docs: add seller product register and query API test scenarios
dnwls16071 Jul 14, 2026
48fafe8
feat: implement seller product registration and lookup API
dnwls16071 Jul 14, 2026
ebe9c4d
docs: add seller product list query API test scenarios
dnwls16071 Jul 14, 2026
2b1e350
feat: implement seller product list query API
dnwls16071 Jul 14, 2026
5911d9a
docs: add shopper product catalog API test scenarios
dnwls16071 Jul 15, 2026
84faaec
feat: implement shopper product catalog browsing API
dnwls16071 Jul 15, 2026
1307590
docs: add contact email test scenarios for product inquiry
dnwls16071 Jul 15, 2026
10788bf
docs: add contact email test scenarios for seller and shopper APIs
dnwls16071 Jul 15, 2026
d3f6e44
feat: implement seller contact email change API
dnwls16071 Jul 15, 2026
75d6a68
ci: add GitHub Actions Gradle build workflow
dnwls16071 Jul 15, 2026
fec3c27
test: add shopper token issuance failure contract tests
dnwls16071 Jul 15, 2026
2b4e68f
test: add shopper issueToken input validation contracts
dnwls16071 Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI with Gradle

on:
push:
branches: ["main"]
pull_request:
branches: ['**']
workflow_dispatch:

jobs:
build:
name: Build with Gradle
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'corretto'

- name: Grant execute permission to Gradle wrapper
run: chmod +x gradlew

- name: Build with Gradle wrapper
run: ./gradlew build

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: '**/build/reports/tests/'
108 changes: 107 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,110 @@
- password 속성이 올바른 형식을 따르지 않으면 400 Bad Request 상태코드를 반환한다.
- email 속성에 이미 존재하는 이메일 주소가 지정되면 400 Bad Request 상태코드를 반환한다.
- username 속성에 이미 존재하는 사용자이름이 지정되면 400 Bad Request 상태코드를 반환한다.
- 비밀번호를 올바르게 암호화한다.
- 비밀번호를 올바르게 암호화한다.

# 판매자 접근 토큰 발행 API 테스트 시나리오 목록

- 올바르게 요청하면 200 OK 상태코드를 반환한다.
- 올바르게 요청하면 액세스 토큰을 반환한다.
- 액세스 토큰은 JWT 형식을 따른다.
- 존재하지 않는 이메일 주소가 사용되면 400 Bad Request 상태코드를 반환한다.
- 잘못된 비밀번호가 사용되면 400 Bad Request 상태코드를 반환한다.

# 구매자 회원 가입 정책

- 이메일 주소는 유일해야 한다.
- 사용자 이름은 유일해야 한다.
- 사용자 이름은 3자 이상의 영문자, 숫자, 하이픈, 밑줄 문자로 구성되어야 한다.
- 비밀번호는 8자 이상의 문자로 구성되어야 한다.

# 구매자 회원 가입 API 테스트 시나리오 목록

- 올바르게 요청하면 204 No Content 상태코드를 반환한다.
- email 속성이 지정되지 않으면 400 Bad Request 상태코드를 반환한다.
- email 속성이 올바른 형식을 따르지 않으면 400 Bad Request 상태코드를 반환한다.
- username 속성이 지정되지 않으면 400 Bad Request 상태코드를 반환한다.
- username 속성이 올바른 형식을 따르지 않으면 400 Bad Request 상태코드를 반환한다.
- username 속성이 올바른 형식을 따르면 204 No Content 상태코드를 반환한다.
- password 속성이 지정되지 않으면 400 Bad Request 상태코드를 반환한다.
- password 속성이 올바른 형식을 따르지 않으면 400 Bad Request 상태코드를 반환한다.
- email 속성에 이미 존재하는 이메일 주소가 지정되면 400 Bad Request 상태코드를 반환한다.
- username 속성에 이미 존재하는 사용자 이름이 지정되면 400 Bad Request 상태코드를 반환한다.
- 비밀번호를 올바르게 암호화한다.

# 판매자 정보 조회 API 테스트 시나리오 목록

- 올바르게 요청하면 200 OK 상태코드를 반환한다.
- 액세스 토큰을 사용하지 않으면 401 Unauthorized 상태코드를 반환한다.
- 서로 다른 판매자의 식별자는 서로 다르다.
- 같은 판매자의 식별자는 항상 같아야 한다.
- 판매자의 기본 정보가 올바르게 설정된다.

# 구매자 정보 조회 API 테스트 시나리오 목록

- 올바르게 요청하면 200 OK 상태코드를 반환한다.
- 액세스 토큰을 사용하지 않으면 401 Unauthorized 상태코드를 반환한다.
- 서로 다른 구매자의 식별자는 서로 다르다.
- 같은 구매자의 식별자는 항상 같아야 한다.
- 구매자의 기본 정보가 올바르게 설정된다.

# 판매자 상품 등록 API 테스트 시나리오 목록

- 올바르게 요청하면 201 Created 상태코드를 반환한다.
- 판매자가 아닌 사용자의 접근 토큰을 사용하면 403 Forbidden 상태코드를 반환한다.
- imageUri 속성이 URI 형식을 따르지 않으면 400 Bad Request 상태코드를 반환한다.
- 올바르게 요청하면 등록된 상품 정보에 접근하는 Location 헤더를 반환한다.

# 판매자 상품 조회 API 테스트 시나리오 목록

- 올바르게 요청하면 200 OK 상태코드를 반환한다.
- 판매자가 아닌 사용자의 접근 토큰을 사용하면 403 Forbidden 상태코드를 반환한다.
- 존재하지 않는 식별자를 사용하면 404 Not Found 상태코드를 반환한다.
- 다른 판매자가 등록한 상품 식별자를 사용하면 404 Not Found 상태코드를 반환한다.
- 상품 식별자를 올바르게 반환한다.
- 상품 정보를 올바르게 반환한다.
- 상품 등록 시각을 올바르게 반환한다.

# 판매자 상품 목록 조회 API 테스트 시나리오 목록

- 올바르게 요청하면 200 OK 상태코드를 반환한다.
- 판매자가 등록한 모든 상품을 반환한다.
- 다른 판매자가 등록한 상품이 포함되지 않는다.
- 상품 정보를 올바르게 반환한다.
- 상품 등록 시각을 올바르게 반환한다.
- 상품 목록을 등록 시점 역순으로 정렬한다.

# 구매자 상품 탐색 API 테스트 시나리오 목록

- 올바르게 요청하면 200 OK 상태코드를 반환한다.
- 판매자 접근 토큰을 사용하면 403 Forbidden 상태코드를 반환한다.
- 첫 번째 페이지의 상품을 반환한다.
- 상품 목록을 등록 시점 역순으로 정렬한다.
- 상품 속성을 올바르게 반환한다.
- 판매자 정보를 올바르게 반환한다.
- 두 번째 페이지를 올바르게 반환한다.
- 마지막 페이지를 올바르게 반환한다.

# 상품 문의 메일을 전송하기 위해 필요한 요구사항

- 구매자가 상품에 대해 궁금한 점이 있을 때 상품 정보에서 문의 이메일 주소를 확인한다.
- 판매자가 회원 가입할 때 문의 이메일 주소를 입력한다.
- 판매자가 자신의 정보를 조회할 때 문의 이메일 주소를 확인한다.

# 판매자 회원 가입 API 문의 이메일 주소 테스트 시나리오

- contactEmail 속성이 올바르게 지정되지 않으면 400 Bad Request 상태코드를 반환한다.

# 판매자 정보 조회 API 문의 이메일 주소 테스트 시나리오

- 판매자의 문의 이메일 주소가 올바르게 설정된다.

# 구매자 상품 탐색 API 문의 이메일 주소 테스트 시나리오

- 판매자의 문의 이메일 주소가 올바르게 설정된다.

# 판매자 문의 이메일 주소 변경 API 테스트 시나리오 목록

- 올바르게 요청하면 204 No Content 상태코드를 반환한다.
- contactEmail 속성이 올바르게 지정되지 않으면 400 Bad Request 상태코드를 반환한다.
- 문의 이메일 주소를 올바르게 변경한다.
39 changes: 24 additions & 15 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
plugins {
id 'java'
id 'org.springframework.boot' version '4.1.0'
id 'io.spring.dependency-management' version '1.1.7'
id 'java'
id 'org.springframework.boot' version '4.1.0'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.study'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testCompileOnly 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-resttestclient'
testImplementation 'org.springframework.boot:spring-boot-restclient'
testCompileOnly 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'
}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.study.tdd.api.controller;

import java.security.Principal;
import java.util.UUID;

import com.study.tdd.api.controller.request.ChangeContactEmailRequest;
import com.study.tdd.application.SellerContactEmailService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SellerChangeContactEmailController {

private final SellerContactEmailService sellerContactEmailService;

@Autowired
public SellerChangeContactEmailController(
SellerContactEmailService sellerContactEmailService
) {
this.sellerContactEmailService = sellerContactEmailService;
}

@PostMapping("/seller/changeContactEmail")
ResponseEntity<?> changeContactEmail(
@RequestBody ChangeContactEmailRequest request,
Principal user
) {
UUID sellerId = UUID.fromString(user.getName());
sellerContactEmailService.changeContactEmail(sellerId, request.toCommand());
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.study.tdd.api.controller;

import com.study.tdd.api.controller.request.IssueSellerTokenRequest;
import com.study.tdd.api.controller.response.AccessTokenCarrier;
import com.study.tdd.application.SellerIssueTokenService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SellerIssueTokenController {

private final SellerIssueTokenService sellerIssueTokenService;

@Autowired
public SellerIssueTokenController(SellerIssueTokenService sellerIssueTokenService) {
this.sellerIssueTokenService = sellerIssueTokenService;
}

@PostMapping("/seller/issueToken")
ResponseEntity<?> issueToken(@RequestBody IssueSellerTokenRequest request) {
return sellerIssueTokenService
.issueToken(request.toQuery())
.map(AccessTokenCarrier::new)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.badRequest().build());
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/study/tdd/api/controller/SellerMeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.study.tdd.api.controller;

import java.security.Principal;
import java.util.UUID;

import com.study.tdd.api.controller.response.SellerMeView;
import com.study.tdd.application.SellerProfileService;
import com.study.tdd.domain.Seller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SellerMeController {

private final SellerProfileService sellerProfileService;

@Autowired
public SellerMeController(SellerProfileService sellerProfileService) {
this.sellerProfileService = sellerProfileService;
}

@GetMapping("/seller/me")
SellerMeView me(Principal user) {
UUID id = UUID.fromString(user.getName());
Seller seller = sellerProfileService.findSeller(id).orElseThrow();
return new SellerMeView(
id,
seller.getEmail(),
seller.getUsername(),
seller.getContactEmail()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.study.tdd.api.controller;

import java.security.Principal;
import java.util.UUID;

import com.study.tdd.api.controller.response.SellerProductView;
import com.study.tdd.application.SellerProductQueryService;
import com.study.tdd.application.query.FindSellerProduct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SellerProductController {

private final SellerProductQueryService sellerProductQueryService;

@Autowired
public SellerProductController(SellerProductQueryService sellerProductQueryService) {
this.sellerProductQueryService = sellerProductQueryService;
}

@GetMapping("/seller/products/{id}")
ResponseEntity<SellerProductView> findProduct(
@PathVariable UUID id,
Principal user
) {
UUID sellerId = UUID.fromString(user.getName());
return ResponseEntity.of(
sellerProductQueryService
.findProduct(new FindSellerProduct(sellerId, id))
.map(SellerProductView::from)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.study.tdd.api.controller;

import java.security.Principal;
import java.util.UUID;

import com.study.tdd.api.controller.response.ArrayCarrier;
import com.study.tdd.api.controller.response.SellerProductView;
import com.study.tdd.application.SellerProductQueryService;
import com.study.tdd.application.query.GetSellerProducts;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SellerProductsController {

private final SellerProductQueryService sellerProductQueryService;

@Autowired
public SellerProductsController(SellerProductQueryService sellerProductQueryService) {
this.sellerProductQueryService = sellerProductQueryService;
}

@GetMapping("/seller/products")
ArrayCarrier<SellerProductView> getProducts(Principal user) {
UUID sellerId = UUID.fromString(user.getName());
SellerProductView[] items = sellerProductQueryService
.getProducts(new GetSellerProducts(sellerId))
.stream()
.map(SellerProductView::from)
.toArray(SellerProductView[]::new);
return new ArrayCarrier<>(items);
}
}
Loading
Loading