π src
β π components // μ¬μ¬μ© κ°λ₯ν UI contribute
β π pages // λΌμ°νΈμ λ§€νλλ μ£Όμ νμ΄μ§
β π routes // React Router μ€μ
β π services // API ν΅μ λ° μΈλΆ μλΉμ€
β πΎ App.jsx // λ£¨νΈ contribute
React Routerλ₯Ό μ¬μ©ν SPA(Single Page Application) λΌμ°νΈμ λλ€.
| κ±΄λ° | μ€λͺ |
|---|---|
/ β /login |
μ κ·Ό μ λ‘κ·ΈμΈ νμ΄μ§λ‘ redirection |
/home |
λ©μΈ ν νμ΄μ§ |
/books |
μ 체 λμ λͺ©λ‘ |
/books/:id |
νΉμ λμ μμΈ |
/create |
λμ μμ± |
/edit/:id |
λμ νΈμ§ |
/my |
μ¬μ©μκ° λ±λ‘ν λμ |
/ai-cover |
AI μ»€λ² μ΄λ―Έμ§ μμ± νμ΄μ§ |
π Navigateλ₯Ό μ¬μ©νμ¬ /λ‘ μ κ·Ό μ /loginμΌλ‘ redirection μ²λ¦¬
OpenAIμ DALLΒ·E λͺ¨λΈμ μ¬μ©ν΄ ν μ€νΈ ν둬ννΈλ‘ μ»€λ² μ΄λ―Έμ§λ₯Ό μμ±ν©λλ€.
export async function generateImageFromPrompt(apiKey, prompt, model, quality, style, size)
| νλΌλ―Έν° | μ€λͺ |
|---|---|
apiKey |
OpenAI API ν€ |
prompt |
μ΄λ―Έμ§ μμ± νλ‘ ννΈ |
model |
μ¬μ© λͺ¨λΈ (dall-e-3, dall-e-2) |
quality |
standard or hd (dall-e-3 μ μ©) |
style |
vivid or natural (dall-e-3 μ μ©) |
size |
μ΄λ―Έμ§ ν¬κΈ° (κΈ°λ³Έ: 1024x1024) |
dall-e-3κ³Όdall-e-2μ API μ€ν μ°¨μ΄λ₯Ό λ°μνμ¬ μ΅μ λμ μ€μ - μλ΅ λ°μ΄ν°κ° μκ±°λ μ€λ₯ μ μμΈ μ²λ¦¬
- μ ν¨νμ§ μμ μ¬μ΄μ¦ μμ² μ κΈ°λ³Έκ°μΌλ‘ μλ λ³κ²½
const url = await generateImageFromPrompt(
OPENAI_API_KEY,
'A fantasy-style magical book cover with dragons and castles'
);
π μ¬μ©μ
βββ [λ‘κ·ΈμΈ]
βββ /home β /create
βββ [νλ‘ ννΈ μ
λ ₯ + generateImageFromPrompt νΈμΆ]
βββ OpenAI API νΈμΆ
βββ μ΄λ―Έμ§ URL λ°ν
βββ μ»€λ² μ΄λ―Έμ§ 미리보기 λ° μ μ₯
- μ΄λ―Έμ§ μλ΅ μ€λ₯ μ
throw new Error(...)λ‘ μμΈ λ°μ β ν둬ννΈμμ Toast Notification λ± μ²λ¦¬ νμ - API Keyλ
.envλ±μ ν΅ν΄ 보μ κ΄λ¦¬ κΆμ₯
- νλ‘ ννΈ μΆμ² κΈ°λ₯ μΆκ° (μ₯λ₯΄ κΈ°λ° μλ μ μ)
- μ¬μ©μ μμ± μ΄λ―Έμ§ μ μ₯ μ΄λ ₯ μ‘°ν κΈ°λ₯
- μμ± μ΄λ―Έμ§ μμ μ΅μ (re-roll, μ μ€μΌμΌ λ±)
| κΈ°μ | μ€λͺ |
|---|---|
React |
UI νλ μμν¬ |
React Router DOM |
SPA λΌμ°νΈ κ΄λ¦¬ |
fetch API |
μ΄λ―Έμ§ μμ±μ© OpenAI API μμ² |
OpenAI DALLΒ·E |
ν μ€νΈ-μ΄λ―Έμ§ μμ± λͺ¨λΈ |
Spring MVC μ€μ ν΄λμ€.
μ£Όμ μν :
- CORS μ€μ μ ν΅ν΄ **React νλ‘ νΈμλ(ν¬νΈ 3000)**μμμ μμ² νμ©
π νμ© λλ©μΈ: <http://localhost:3000>
π νμ© λ©μλ: GET, POST, PUT, PATCH, DELETE, OPTIONS
π allowCredentials: true
/api/books κ²½λ‘λ‘ λμ κ΄λ ¨ API μ 곡.
| HTTP | URL | κΈ°λ₯ |
|---|---|---|
| POST | /api/books |
λμ λ±λ‘ |
| GET | /api/books |
μ 체 λμ λͺ©λ‘ μ‘°ν |
| GET | /api/books/{id} |
νΉμ λμ μμΈ μ‘°ν |
| PUT | /api/books/{id} |
λμ μ 보 μμ |
| DELETE | /api/books/{id} |
λμ μμ |
κΈ°ν νΉμ§:
@CrossOriginμΌλ‘ CORS νμ© (localhost:3000)BookServiceμ μ€μ λ‘μ§ μμ
/api/users κ²½λ‘λ‘ μ¬μ©μ κ΄λ ¨ API μ 곡.
| HTTP | URL | κΈ°λ₯ |
|---|---|---|
| POST | /api/users |
μ¬μ©μ λ±λ‘ |
| GET | /api/users |
μ 체 μ¬μ©μ μ‘°ν |
| POST | /api/users/login |
μ¬μ©μ λ‘κ·ΈμΈ (μ΄λ©μΌ/λΉλ°λ²νΈ κΈ°λ°) |
κΈ°ν νΉμ§:
LoginFailedExceptionμμΈ λ°μ μ 401 μλ΅ μ²λ¦¬UserServiceλ₯Ό ν΅ν΄ μ¬μ©μ λ‘μ§ μ²λ¦¬- μλ΅ μ
UserResponseDto,UserLoginResponseDtoμ¬μ©
β DTO μ€λͺ (Data Transfer Object)
λμ λ° μ¬μ©μ κ΄λ ¨ λ°μ΄ν°λ₯Ό μμ²νκ±°λ μλ΅ν λ μ¬μ©νλ κ°μ²΄μ λλ€. Controller β Service β Entity κ° λ°μ΄ν° μ λ¬μ μ¬μ©λ©λλ€.
-
UserRequestDtoμ¬μ©μ λ±λ‘ μμ²μ μ¬μ©.
νλ:
username,email,password -
UserResponseDtoμ¬μ©μ μ 보 μλ΅μ μ¬μ©.
νλ:
userId,username,email -
UserLoginResponseDtoλ‘κ·ΈμΈ μλ΅ μ μ¬μ©.
νλ:
token(JWT λ±),user(UserResponseDto)
-
BookRequestDtoλμ λ±λ‘/μμ μμ²μ μ¬μ©.
νλ:
title,description,coverImageUrl,userId -
BookResponseDtoλμ μ 보 μλ΅μ μ¬μ©.
νλ:
id,title,description,coverImageUrl,createdAt,updatedAt,userId,username
-
ErrorResponseμλ¬ μλ΅ ν¬λ§·.
νλ:
code(HTTP μν μ½λ),status(errorλ±),message(μλ¬ λ©μμ§)
- μν : λμ κ΄λ ¨ λΉμ¦λμ€ λ‘μ§ μ²λ¦¬
- μ£Όμ λ©μλ:
createBook(BookRequestDto dto): λμ λ±λ‘ (userIdλ₯Ό λ°μUsersμ μ°κ΄ μ§μ)getAllBooks(): λͺ¨λ λμ μ‘°νgetBook(Long id): λ¨μΌ λμ μμΈ μ‘°νupdateBook(Long id, BookRequestDto dto): λμ μμ deleteBook(Long id): λμ μμ
- νΉμ§:
BookRepositoryμUserRepositoryλ₯Ό νμ©- DTO β Entity λ³ν
UsersμBookκ°μ μ°κ΄ κ΄κ³ λ§€ν μ¬μ©
- μν : μ¬μ©μ κ΄λ ¨ λΉμ¦λμ€ λ‘μ§ μ²λ¦¬
- μ£Όμ λ©μλ:
createUser(UserRequestDto dto): μ¬μ©μ νμκ°μgetAllUsers(): μ 체 μ¬μ©μ λͺ©λ‘ μ‘°νlogin(UserRequestDto dto): μ¬μ©μ λ‘κ·ΈμΈ (λ¨μ λ‘κ·ΈμΈ λ‘μ§ / JWT λ―Έν¬ν¨)
- νΉμ§:
- μ¬μ©μ μΈμ¦ μ²λ¦¬ (μ€ν¨ μ
LoginFailedExceptionλ°μ) - μ¬μ©μ μ 보 μλ΅ μ
UserResponseDtoμ¬μ©
- μ¬μ©μ μΈμ¦ μ²λ¦¬ (μ€ν¨ μ
@ManyToOneβUsers(λμλ₯Ό μμ±ν μ¬μ©μ)- νλ:
title,description,coverImageUrl,createdAt,updatedAt
@OneToMany(mappedBy = "user")βList<Book>- νλ:
username,email,password,book(λμ λͺ©λ‘)
π¦ Controller
βββ BookController
βββ BookService
βββ BookRepository
βββ UserRepository (userId β Users μ‘°ν)
π¦ Controller
βββ UserController
βββ UserService
βββ UserRepository
Spring Data JPAλ₯Ό νμ©νμ¬ λ°μ΄ν°λ² μ΄μ€ μ κ·Όμ μΆμνν Repository κ³μΈ΅μ λλ€. 볡μ‘ν SQL μμ± μμ΄ λ©μλ λ€μ΄λ°λ§μΌλ‘ 쿼리λ₯Ό μλ μμ±ν μ μμ΅λλ€.
- λμ μ 보λ₯Ό κ΄λ¦¬νλ JPA Repository
JpaRepository<Book, Long>μμ β κΈ°λ³Έμ μΈ CRUD μ 곡
public interface BookRepository extends JpaRepository<Book, Long> {
// νμμ 컀μ€ν
쿼리 μΆκ° κ°λ₯
}- μ¬μ©μ μ 보λ₯Ό κ΄λ¦¬νλ JPA Repository
- μ΄λ©μΌ κΈ°λ° μ‘°ν κΈ°λ₯μ μν 컀μ€ν λ©μλ μΆκ°
public interface UserRepository extends JpaRepository<Users, Long> {
Optional<Users> findByEmail(String email);
}π κΈ°λ₯ μμ½
| λ©μλ | μ€λͺ |
|---|---|
findByEmail(String email) |
μ¬μ©μμ μ΄λ©μΌμ κΈ°λ°μΌλ‘ Optionalλ‘ μ‘°ν |
| κΈ°λ³Έ μ 곡 λ©μλ | findById, findAll, save, deleteById λ± |
- λΉμ¦λμ€ λ‘μ§κ³Ό DB λ‘μ§ λΆλ¦¬
- SQL μμ± μμ΄ λΉ λ₯Έ κ°λ° κ°λ₯
- νμμ
@Queryλλ Querydslλ‘ νμ₯ κ°λ₯
λΉμ¦λμ€ λ‘μ§μ μ²λ¦¬νλ ν΅μ¬ κ³μΈ΅μ λλ€. DTOλ₯Ό ν΅ν΄ 컨νΈλ‘€λ¬μμ λ°μ΄ν° νλ¦μ κ΄λ¦¬νκ³ , DB μ κ·Όμ Repositoryλ₯Ό ν΅ν΄ μνν©λλ€.
π μ± CRUD + μ μ μ°λ
| λ©μλ | μ€λͺ |
|---|---|
createBook(BookRequestDto dto) |
μ± μμ± (μ λͺ©, μ€λͺ , μ΄λ―Έμ§ URL, μ μ ID νμ) |
getAllBooks() |
μ 체 μ± λͺ©λ‘ μ‘°ν |
getBook(Long id) |
IDλ‘ μ± μ‘°ν |
updateBook(Long id, BookRequestDto dto) |
μ± μ 보 μμ |
deleteBook(Long id) |
μ± μμ |
π§ μ£Όμ νΉμ§:
BookRequestDtoβ μν°ν° β μ μ₯ βBookResponseDtoλ³ν- μ± μ μ₯ μ μ μ μ‘΄μ¬ μ¬λΆ νμΈ
LocalDateTime.now()μΌλ‘createdAt,updatedAtμλ μ€μ - μμΈ μ²λ¦¬:
BookCreateException: μ λ ₯ κ° μ ν¨μ± μ€ν¨BookNotFoundException: ν΄λΉ IDμ μ± μ΄ μλ κ²½μ°
π€ μ μ μ‘°ν + λ‘κ·ΈμΈ (νμκ°μ μμ)
| λ©μλ | μ€λͺ |
|---|---|
getAllUsers() |
μ 체 μ μ λͺ©λ‘ μ‘°ν |
getUser(Long userId) |
IDλ‘ μ μ μ‘°ν |
deleteUser(Long userId) |
μ μ μμ |
login(UserRequestDto dto) |
μ΄λ©μΌ & ν¨μ€μλ κΈ°λ° λ‘κ·ΈμΈ, μμ ν ν° λ°κΈ |
π λ‘κ·ΈμΈ μμΈ:
- μ΄λ©μΌλ‘ μ μ μ‘°ν (
findByEmail) - λΉλ°λ²νΈ μΌμΉ μ¬λΆ νμΈ
- μ±κ³΅ μ
UserLoginResponseDtoλ°ν (ν ν° + μ μ μ 보) - μ€ν¨ μ
LoginFailedExceptionμμΈ λ°μ
λ‘κ·ΈμΈμ μ¬μ μ DBμ μ μ₯λ μ μ μ 보λ§μ κΈ°λ°μΌλ‘ μλν©λλ€.
- DTO β Entity λͺ ν λΆλ¦¬
- μμΈ μ²λ¦¬λ‘ μ¬μ©μ κ²½ν κ°μ
- μΆν νμ₯ μ©μ΄ (μ: JWT ν ν° λ°κΈ, νμκ°μ μΆκ° κ°λ₯)
spring:
datasource:
url: jdbc:h2:file:~/librarytestdb
driver-class-name: org.h2.Driver
username: sa
password: 1234
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
defer-datasource-initialization: true
h2:
console:
enabled: true
path: /h2-console
sql:
init:
mode: always
server:
port: 8081
- H2 DB: νμΌ λͺ¨λ (
~/librarytestdb) - μΉ μ½μ URL: http://localhost:8081/h2-console
- μ μ μ 보:
- JDBC URL:
jdbc:h2:file:~/librarytestdb - μ¬μ©μλͺ
:
sa - λΉλ°λ²νΈ:
1234
- JDBC URL:
- JPA ddl-auto:
createβ μλ² μ€ν μ λ§€λ² ν μ΄λΈ μλ‘ μμ± - μλ² ν¬νΈ:
8081
β οΈ νμκ°μ νμ΄μ§λ μ 곡νμ§ μμΌλ©°,H2 DBμ 미리 λ±λ‘λ μ μ μ 보λ‘λ§ λ‘κ·ΈμΈμ΄ κ°λ₯ν©λλ€.
- λ‘κ·ΈμΈ ν
μ€νΈλ₯Ό μν΄
resources/data.sqlμ μ¬μ©μ μ 보μ μ± λ°μ΄ν°λ₯Ό μ¬μ μ½μ ν΄μΌ ν©λλ€. - μ§μ H2 μ½μμμ μ½μ
ν΄λ κ°λ₯νμ§λ§, μ΄κΈ°ν μλνλ₯Ό μν΄
data.sqlμ¬μ©μ κΆμ₯ν©λλ€.
-- μ μ λ°μ΄ν° μ½μ
INSERT INTO users (user_id, username, email, password) VALUES
(1, 'testuser', 'test@example.com', 'password123'),
(2, 'anotheruser', 'another@example.com', 'password456');
-- λμ λ°μ΄ν° μ½μ
INSERT INTO book (title, description, cover_image_url, created_at, updated_at, user_id) VALUES
('Spring Boot in Action', 'Spring Boot μ€λͺ
μ', '<https://example.com/spring.jpg>', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1),
('JPA with Hibernate', 'JPAλ₯Ό νμ©ν ORM μ€λͺ
μ', '<https://example.com/jpa.jpg>', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1),
('React for Beginners', 'νλ‘ νΈμλ μ
λ¬Έμ', '<https://example.com/react.jpg>', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 2);
usersν μ΄λΈμuser_idλ μ§μ μ§μ bookν μ΄λΈμidλ auto-incrementλΌ μλ΅- κ° λμμ
user_idλ λ±λ‘λ μ¬μ©μμ μ°κ΄
λ³Έ νλ‘μ νΈλ μμΈ μν©μ λͺ νν λΆλ¦¬νκ³ , μ¬μ©μμκ² μλ―Έ μλ μλ΅μ μ 곡νκΈ° μν΄ μ»€μ€ν μμΈ ν΄λμ€μ **μ μ μμΈ μ²λ¦¬κΈ°(GlobalExceptionHandler)**λ₯Ό μ¬μ©ν©λλ€.
| μμΈ ν΄λμ€λͺ | μ€λͺ |
|---|---|
BookNotFoundException |
μμ²ν λμλ₯Ό μ°Ύμ μ μμ λ λ°μ (404) |
BookCreateException |
λμ μμ± μ€ μ€λ₯κ° λ°μνμ λ |
CoverNotFoundException |
λμ νμ§ μ΄λ―Έμ§κ° μ‘΄μ¬νμ§ μμ λ |
CoverGenerateException |
λμ νμ§ μμ± μ€ μ€λ₯κ° λ°μνμ λ |
LoginFailedException |
λ‘κ·ΈμΈ μ λ³΄κ° μΌμΉνμ§ μμ λ (401) |
UserNotFoundException |
νΉμ μ¬μ©μλ₯Ό μ°Ύμ μ μμ λ λ°μ (404) |
public class LoginFailedException extends RuntimeException {
public LoginFailedException(String message) {
super(message);
}
}@RestControllerAdviceλ₯Ό ν΅ν΄ λͺ¨λ 컨νΈλ‘€λ¬μμ λ°μνλ μμΈλ₯Ό μΌκ΄ μ²λ¦¬ν©λλ€.
| μμΈ νμ | HTTP μνμ½λ | μλ΅ λ©μμ§ μμ |
|---|---|---|
BookNotFoundException, UserNotFoundException |
404 Not Found |
ν΄λΉ 리μμ€κ° μ‘΄μ¬νμ§ μμ΅λλ€: [message] |
LoginFailedException |
401 Unauthorized |
λ‘κ·ΈμΈ μ€ν¨: [message] (μ§μ νΈλ€λ§ κ°λ₯) |
IllegalArgumentException |
400 Bad Request |
μλͺ»λ μμ²μ
λλ€: [message] |
| κ·Έ μΈ λͺ¨λ μμΈ | 500 Internal Server Error |
μλ² λ΄λΆ μ€λ₯κ° λ°μνμ΅λλ€. |
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body("ν΄λΉ μ¬μ©μκ° μ‘΄μ¬νμ§ μμ΅λλ€: " + ex.getMessage());
}πΈ Note: LoginFailedExceptionμ UserController λ΄λΆμμ try-catchλ‘ μ²λ¦¬λμ΄ μμΌλ©°, μΆν μ μ μμΈ μ²λ¦¬κΈ°λ‘ μ΄λμν¬ μ μμ΅λλ€.
- μμΈλ³ λ©μμ§ λ° μνμ½λ λͺ ννκ² κ΄λ¦¬
- μ½λ μ€λ³΅ μ κ±° λ° μ μ§λ³΄μ μ©μ΄
- μ¬μ©μ μ€μ¬μ μλ΅ μ 곡μΌλ‘ UX ν₯μ