Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
80cce6a
docs: GlobalExceptionHandler class java docs ์ถ”๊ฐ€
haazz Oct 19, 2025
d7f66c6
build: jakarta validation dependency ์ถ”๊ฐ€
haazz Oct 19, 2025
a607f01
feat: ์ƒ์  ๋“ฑ๋ก
haazz Oct 19, 2025
e6f5782
refactor: StoreCreateRequest class record๋กœ ๋ณ€๊ฒฝ
haazz Oct 21, 2025
0eea5b5
feat: ์ƒ์  ์ „์ฒด ์กฐํšŒ
haazz Oct 21, 2025
2fc1d20
feat: ์ƒ์  ๋‹จ์ผ ์กฐํšŒ
haazz Oct 21, 2025
0a7b9d0
feat: ์ƒ์  ์ˆ˜์ •
haazz Oct 22, 2025
a4d0bf6
refactor: StoreCreateRequest class ํ•„๋“œ๋ช… ๋ณ€๊ฒฝ
haazz Oct 22, 2025
7ef8047
feat: ์ƒ์  ์‚ญ์ œ
haazz Oct 22, 2025
b1fa813
feat: ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€
haazz Oct 22, 2025
12d0f9c
refactor: null ์ฒดํฌ ๋ฐฉ์‹ ๊ฐœ์„ 
haazz Oct 22, 2025
96ac439
feat: TestSecurityConfig class
haazz Oct 23, 2025
fb0e7e3
feat: StoreControllerTest class ์ƒ์  ๋“ฑ๋ก ํ…Œ์ŠคํŠธ
haazz Oct 23, 2025
81db18d
feat: StoreControllerTest class ์ƒ์  ์กฐํšŒ ํ…Œ์ŠคํŠธ
haazz Oct 25, 2025
aba433b
feat: StoreControllerTest class ์ƒ์  ์ˆ˜์ • ํ…Œ์ŠคํŠธ
haazz Oct 29, 2025
c99ebb4
feat: StoreControllerTest class ์ƒ์  ์‚ญ์ œ ํ…Œ์ŠคํŠธ
haazz Oct 29, 2025
38290a5
chore: GlobalExceptionHandler class ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ˆ˜์ •
haazz Oct 29, 2025
c1069da
feat: StoreControllerTest class ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
haazz Oct 29, 2025
d489150
feat: StoreServiceTest class createStore() ํ…Œ์ŠคํŠธ
haazz Nov 3, 2025
06f94a4
chore: StoreService class ์˜คํƒ€ ์ˆ˜์ •
haazz Nov 6, 2025
c10c06e
feat: StoreServiceTest class modifyStore() ํ…Œ์ŠคํŠธ
haazz Nov 6, 2025
c120036
feat: StoreServiceTest class createStore() ํ…Œ์ŠคํŠธ
haazz Nov 6, 2025
f5060b7
feat: StoreServiceTest class deleteStore() ํ…Œ์ŠคํŠธ
haazz Nov 6, 2025
f5bbf41
feat: StoreResponse class ownerId ์ถ”๊ฐ€
haazz Nov 6, 2025
760a0c7
feat: StoreServiceTest class getStore() ํ…Œ์ŠคํŠธ
haazz Nov 6, 2025
96d9b33
chore: StoreServiceTest class NON_EXISTENT_ID ์‚ญ์ œ
haazz Nov 6, 2025
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
1 change: 1 addition & 0 deletions sunpick/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'
implementation 'mysql:mysql-connector-java:8.0.33'
compileOnly 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.backend.sunpick.domain.store.controller;

import com.backend.sunpick.domain.store.dto.request.StoreCreateRequest;
import com.backend.sunpick.domain.store.dto.request.StoreModifyRequest;
import com.backend.sunpick.domain.store.dto.response.StoreResponse;
import com.backend.sunpick.domain.store.service.StoreService;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/store")
public class StoreController {

private final StoreService storeService;

@PostMapping
public ResponseEntity<Void> createStore(@RequestBody @Valid StoreCreateRequest request) {
storeService.createStore(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}

@GetMapping
public ResponseEntity<List<StoreResponse>> getStoreAll() {
List<StoreResponse> response = storeService.getStoreAll();
return ResponseEntity.status(HttpStatus.OK).body(response);
}

@GetMapping("/{storeId}")
public ResponseEntity<StoreResponse> getStoreById(@PathVariable Integer storeId) {
StoreResponse response = storeService.getStoreById(storeId);
return ResponseEntity.status(HttpStatus.OK).body(response);
}

@PatchMapping("/{storeId}")
public ResponseEntity<Void> modifyStore(@PathVariable Integer storeId, @RequestBody @Valid StoreModifyRequest request) {
storeService.modifyStore(storeId, request);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}

@DeleteMapping("/{storeId}")
public ResponseEntity<Void> deleteStore(@PathVariable Integer storeId) {
storeService.deleteStore(storeId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.backend.sunpick.domain.store.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;

public record StoreCreateRequest(
@NotNull(message = "ํšŒ์› ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
@Positive(message = "ํšŒ์› ID๋Š” 1 ์ด์ƒ์˜ ๊ฐ’์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
Integer memberId,

@NotBlank(message = "์ƒ์ ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
@Size(max = 20, message = "์ƒ์ ๋ช…์€ 20์ž ์ดํ•˜๋กœ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")
String name,

@Size(max = 100, message = "์ƒ์  ์„ค๋ช…์€ 100์ž ์ดํ•˜๋กœ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")
String description
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.backend.sunpick.domain.store.dto.request;

import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;

public record StoreModifyRequest(
@Positive(message = "ํšŒ์› ID๋Š” 1 ์ด์ƒ์˜ ๊ฐ’์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
Integer memberId,

@Size(max = 20, message = "์ƒ์ ๋ช…์€ 20์ž ์ดํ•˜๋กœ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")
String name,

@Size(max = 100, message = "์ƒ์  ์„ค๋ช…์€ 100์ž ์ดํ•˜๋กœ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")
String description
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.backend.sunpick.domain.store.dto.response;

public record StoreResponse(
Integer id,
String name,
String description,
Integer ownerId,
String ownerName
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.NoSuchElementException;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -40,4 +41,32 @@ public class Store extends BaseEntity {
@Column(name = "owner_name", length = 6, nullable = false)
private String ownerName;

@Column(name = "is_deleted", nullable = false)
private boolean isDeleted;

public void changeName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("์ƒ์ ๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}
this.name = name;
}

public void changeDescription(String description) {
this.description = description;
}

public void changeOwner(Member member) {
if (member == null || member.getName() == null || member.getName().isBlank()) {
throw new IllegalArgumentException("ํšŒ์›๊ณผ ํšŒ์›๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}
this.member = member;
this.ownerName = member.getName();
}

public void delete() {
if (isDeleted) {
throw new NoSuchElementException("์ƒ์  ID: " + id + "๋Š” ์ด๋ฏธ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
}
isDeleted = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.backend.sunpick.domain.store.service;

import com.backend.sunpick.domain.member.entity.Member;
import com.backend.sunpick.domain.member.repository.MemberRepository;
import com.backend.sunpick.domain.store.dto.request.StoreCreateRequest;
import com.backend.sunpick.domain.store.dto.request.StoreModifyRequest;
import com.backend.sunpick.domain.store.dto.response.StoreResponse;
import com.backend.sunpick.domain.store.entity.Store;
import com.backend.sunpick.domain.store.repository.StoreRepository;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class StoreService {

private final StoreRepository storeRepository;
private final MemberRepository memberRepository;

@Transactional
public void createStore(StoreCreateRequest request) {
Member member = memberRepository.findById(request.memberId())
.orElseThrow(() -> new NoSuchElementException(
"ํšŒ์› ID: " + request.memberId() + "๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."));
storeRepository.save(Store.builder()
.member(member)
.name(request.name())
.description(request.description())
.ownerName(member.getName())
.build());
}

@Transactional(readOnly = true)
public List<StoreResponse> getStoreAll() {
return storeRepository.findAll().stream()
.map(this::toResponse)
.toList();
}

@Transactional(readOnly = true)
public StoreResponse getStoreById(Integer storeId) {
Store store = storeRepository.findById(storeId)
.orElseThrow(() -> new NoSuchElementException("์ƒ์  ID: " + storeId + "๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."));
return toResponse(store);
}

@Transactional
public void modifyStore(Integer storeId, StoreModifyRequest request) {
Store store = storeRepository.findById(storeId)
.orElseThrow(() -> new NoSuchElementException("์ƒ์  ID: " + storeId + "๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."));

if (store.isDeleted()) {
throw new NoSuchElementException("์ƒ์  ID: " + storeId + "๋Š” ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
}

Optional.ofNullable(request.memberId()).ifPresent(memberId -> {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new NoSuchElementException(
"ํšŒ์› ID: " + memberId + "๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."));
store.changeOwner(member);
});
Optional.ofNullable(request.name()).ifPresent(store::changeName);
Optional.ofNullable(request.description()).ifPresent(store::changeDescription);
}

@Transactional
public void deleteStore(Integer storeId) {
Store store = storeRepository.findById(storeId)
.orElseThrow(() -> new NoSuchElementException("์ƒ์  ID: " + storeId + "๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."));
store.delete();
}

private StoreResponse toResponse(Store store) {
return new StoreResponse(store.getId(), store.getName(), store.getDescription(),
store.getMember().getId(), store.getOwnerName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
* ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
* <p>
* ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋ฅผ ๊ฐ€๋กœ์ฑ„ ๊ฐ„๋‹จํ•œ HTTP ์ƒํƒœ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋กœ ๋ณ€ํ™˜
* </p>
*
* @author haazz
*/
@RestControllerAdvice
public class GlobalExceptionHandler {

Expand All @@ -30,7 +38,7 @@ public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
Optional<String> fieldMsg = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + messageOrDefault(fe.getDefaultMessage(), "์œ ํšจ์„ฑ ๊ฒ€์ฆ ์˜ค๋ฅ˜"))
.map(fe -> messageOrDefault(fe.getDefaultMessage(), "์œ ํšจ์„ฑ ๊ฒ€์ฆ ์˜ค๋ฅ˜"))
.findFirst();

Optional<String> globalMsg = e.getBindingResult().getGlobalErrors().stream()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.backend.sunpick.global.config;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@TestConfiguration
public class TestSecurityConfig {

@Bean
public SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/store/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
Loading