Skip to content

Latest commit

ย 

History

44 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ”๏ธ Snowthing (๋ˆˆํŒ…)

์ „๊ตญ ์Šคํ‚ค์žฅ ์ •๋ณด์™€ ๋ผ์ด๋”ฉ ๊ฒฝํ—˜์„ ๋‚˜๋ˆ„๊ณ , ํ•จ๊ป˜ ํƒˆ ์œ ์ €๋ฅผ ์ฐพ๋Š” ์œˆํ„ฐ์Šคํฌ์ธ  ์ „์šฉ ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”Œ๋žซํผ


1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” (Overview)

  • ์„œ๋น„์Šค๋ช…: Snowthing (๋ˆˆํŒ…)
  • ๋ชฉ์ : ๊ธฐ์กด ๋…ธํ›„ํ™”๋œ ์œˆํ„ฐ์Šคํฌ์ธ  ์ปค๋ฎค๋‹ˆํ‹ฐ์˜ ๋ถˆํŽธํ•จ(http ์ ‘์†, ๋ณด์•ˆ ์ทจ์•ฝ, ์ด๋ฏธ์ง€ ์ฒจ๋ถ€ ์˜ค๋ฅ˜ ๋“ฑ)์„ ๊ทน๋ณตํ•˜๊ณ , ์Šค๋…ธ๋ณด๋”/์Šคํ‚ค์–ด๋ฅผ ์œ„ํ•œ ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”Œ๋žซํผ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • 1์ฐจ MVP ํ•ต์‹ฌ ํƒ€๊ฒŸ:
    1. ์ˆ™๋ จ ์ด์šฉ์ž (์‹œ์ฆŒ ๋ณด๋”): ์‹ค์‹œ๊ฐ„ ์„ค์งˆ/ํ˜„์žฅ ์ •๋ณด ๊ณต์œ , ํ”„๋กœํ•„ N:M ์ค‘๊ณ„(๋ฒ ์ด์Šค ์Šคํ‚ค์žฅ/๋ผ์ด๋”ฉ ์„ฑํ–ฅ) ๋“ฑ๋ก
    2. ์ดˆ๋ณด ์ด์šฉ์ž (์ž…๋ฌธ ๋ณด๋”): ๋ถ€๋‹ด ์—†๋Š” ๋น„ํšŒ์› ์ต๋ช… ์ž‘์„ฑ ๋ฐ ์ •์ œ๋œ Q&A ์ง€์‹ ํƒ์ƒ‰

2. ๊ธฐ์ˆ  ์Šคํƒ (Tech Stack)

Frontend

  • Core: Next.js 16 (App Router), TypeScript, React 19
  • State & Fetching: React State, Fetch API (credentials: 'include')
  • Styling: Vanilla CSS, UI Component System

Backend

  • Core: Java 21 (LTS), Spring Boot 3.3.x, Spring Data JPA
  • Security & Auth: Spring Security 6, Spring Session (Servlet Memory / Redis Ready), BCrypt
  • Build & Test: Gradle, JUnit5, Mockito, MockMvc

Database & Cache

  • RDBMS: H2 (In-Memory Dev), MySQL 8.0 (Production)
  • In-Memory Cache: Spring Session Redis (Scale-out Ready)

3. ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ & ์ฃผ์š” ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ ๊ณ ๋ฏผ๊ณผ ํ•ด๊ฒฐ (Architecture & Auth Flow)

Snowthing์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX) ์ตœ์šฐ์„ ๊ณผ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ฐ ๋ณด์•ˆ์„ฑ์„ ์ˆ˜ํ˜ธํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„๋ฅผ ์„ ํƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.

sequenceDiagram
    autonumber
    actor User as ๐Ÿ‘ค ์‚ฌ์šฉ์ž (Browser)
    participant Client as ๐Ÿ’ป Next.js (Port 3000)
    participant Server as โš™๏ธ Spring Boot (Port 8080)
    participant Session as ๐Ÿง  Tomcat Session Manager

    Note over User, Server: 1. ๋น„ํšŒ์› ์ ‘์† & ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ์‹œ๋„
    User->>Client: ํผ ์ž…๋ ฅ (์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ, N:M ์ฒดํฌ๋ฐ•์Šค)
    Client->>Server: POST /api/auth/login (with credentials: 'include')
    
    Note over Server: 2. ๋น„๋ฐ€๋ฒˆํ˜ธ ์ •๋ฐ€ ์ •๊ทœ์‹ & BCrypt ํ•ด์‹œ ๊ฒ€์ฆ
    Server->>Server: passwordEncoder.matches(raw, encoded)
    
    Note over Server, Session: 3. ์„ธ์…˜ ๊ณ ์ • ๋ฐฉ์–ด (Session Fixation Defense)
    Server->>Session: request.changeSessionId() ํ˜ธ์ถœ! (์„ธ์…˜ ์‹๋ณ„์ž ๊ต์ฒด)
    Session-->>Server: ์‹ ๊ทœ 32์ž๋ฆฌ JSESSIONID ๋ฐœ๊ธ‰ (๊ธฐ์กด ์Šคํ‚ค์žฅ/์„ฑํ–ฅ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์œ ์ง€)
    
    Server-->>Client: Set-Cookie: JSESSIONID=A1B2...; Path=/; HttpOnly; SameSite=Lax
    Client-->>User: ๋กœ๊ทธ์ธ ์„ฑ๊ณต (๋ฉ”์ธ ํ”„๋กœํ•„ ๋Œ€์‹œ๋ณด๋“œ ์ „ํ™˜)

    Note over User, Server: 4. ์ธ์ฆ๋œ API ์š”์ฒญ (ํ”„๋กœํ•„ ์กฐํšŒ/์ˆ˜์ •)
    User->>Client: ํ”„๋กœํ•„ ์ˆ˜์ • ํด๋ฆญ (PUT /api/members/me)
    Client->>Server: PUT /api/members/me (Cookie: JSESSIONID=A1B2...)
    Server->>Session: JSESSIONID ์„ธ์…˜ ๊ฒ€์ฆ & SecurityContext ๋ณต์›
    Server-->>Client: 200 OK (์ˆ˜์ •๋œ ํ”„๋กœํ•„ & N:M ํƒœ๊ทธ ์นฉ ๋ฐ˜ํ™˜)

    Note over User, Server: 5. ์„ธ์…˜ ๋กœ๊ทธ์•„์›ƒ (3๋‹จ๊ณ„ ์†Œ๋ฉธ ํ”„๋กœ์„ธ์Šค)
    User->>Client: ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ ํด๋ฆญ
    Client->>Server: POST /api/auth/logout
    Server->>Server: 1) ThreadLocal.clearContext() ์ฒญ์†Œ
    Server->>Session: 2) session.invalidate() ํ†ฐ์บฃ ์„ธ์…˜ ํŒŒ๊ธฐ
    Server-->>Client: 3) Set-Cookie: JSESSIONID=; Max-Age=0 (์ฟ ํ‚ค ์ฆ‰์‹œ ๋งŒ๋ฃŒ)
Loading

ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ณ ๋ฏผ ๋ฐ ๊ธฐ์ˆ ์  ์˜์‚ฌ๊ฒฐ์ •

1. ๊ณตํ†ต ์—”ํ‹ฐํ‹ฐ์™€ JPA Auditing (@EnableJpaAuditing) ๋„์ž…

createdAt, updatedAt ์ปฌ๋Ÿผ์€ ํšŒ์›๊ฐ€์ž…, ๊ฒŒ์‹œ๊ธ€, ๋Œ“๊ธ€ ๋“ฑ ์‹œ์Šคํ…œ ์ „์ฒด ๋„๋ฉ”์ธ์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด์—๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ๋ฒจ์—์„œ ์ง์ ‘ set์„ ํ˜ธ์ถœํ•˜๊ฑฐ๋‚˜ DB ๋ ˆ๋ฒจ์˜ sysdate๋ฅผ ์ด์šฉํ•˜๋ ค ํ–ˆ์œผ๋‚˜ ์—ฌ๋Ÿฌ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๊ฐ€ ์กด์žฌํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ˆ˜๋™ ์ฃผ์ž… ๋ฐฉ์‹: ์—”ํ‹ฐํ‹ฐ๋งˆ๋‹ค ์‹œ๊ฐ„ ์ƒ์„ฑ ์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋˜๊ณ , ๊ฐœ๋ฐœ์ž์˜ ์‹ค์ˆ˜๋กœ null์ด ๋“ค์–ด๊ฐˆ ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  2. DB ๋ ˆ๋ฒจ sysdate ์ฃผ์ž… ๋ฐฉ์‹: JPA 1์ฐจ ์บ์‹œ ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ผ๊ฐ„ ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๋Š” ๋‘ ํ•„๋“œ๋ฅผ null๋กœ ๊ฐ€์ง€๊ณ  ์žˆ์–ด, ํšŒ์›๊ฐ€์ž… ์งํ›„ ์‘๋‹ต DTO๋ฅผ ๋ฐ˜ํ™˜ํ•  ๋•Œ ์‹œ๊ฐ„์ด null๋กœ ์ถœ๋ ฅ๋˜๋Š” 1์ฐจ ์บ์‹œ ๋ฉ”๋ชจ๋ฆฌ โ†” DB ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ ๋ฐฉ์•ˆ: BaseTimeEntity ๊ณตํ†ต ์ถ”์ƒ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  JPA ์˜์†์„ฑ ๋ผ์ดํ”„์‚ฌ์ดํด ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์‹œํ•˜๋Š” AuditingEntityListener๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    SQL ํŒจํ‚ท์ด DB๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ๋ฐ”๋กœ ์ „ ์ž๋ฐ” ๋ฉ”๋ชจ๋ฆฌ ๊ฐ์ฒด์˜ ๋‘ ํ•„๋“œ์— ํ˜„์žฌ ์‹œ๊ฐ์„ ๋จผ์ € ์ฑ„์›Œ ๋„ฃ๋„๋ก ์ฒ˜๋ฆฌํ•˜์—ฌ ์ฝ”๋“œ ์ค‘๋ณต๊ณผ 1์ฐจ ์บ์‹œ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜๋ฅผ ๋™์‹œ์— ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.

2. ๋‹จ์ผ ์„ธ์…˜ ์ œํ•œ โž” ๋‹ค์ค‘ ๋””๋ฐ”์ด์Šค ์ง€์› ๋ฐ 30์ผ Remember-Me ์Šฌ๋ผ์ด๋”ฉ ์„ธ์…˜

์ดˆ๊ธฐ์—๋Š” ์˜จ๋ผ์ธ ๊ฒŒ์ž„์ด๋‚˜ ์€ํ–‰ ์•ฑ์ฒ˜๋Ÿผ ๊ณ„์ • ๋„์šฉ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ํ•˜๋‚˜์˜ ์„ธ์…˜๋งŒ ํ—ˆ์šฉํ•˜๋Š” ๋‹จ์ผ ์„ธ์…˜์„ ๊ตฌ์ƒํ–ˆ์œผ๋‚˜, ๋ณธ ์ปค๋ฎค๋‹ˆํ‹ฐ์˜ ํŠน์„ฑ์— ๋งž์ถฐ ์œ ์ €์˜ ์ดํƒˆ๋ฅ ์„ ๋‚ฎ์ถ”๊ณ  ์žฅ์‹œ๊ฐ„ ์ฒด๋ฅ˜ํ•˜๋„๋ก ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์„œ๋น„์Šค์˜ ํ•ต์‹ฌ ๋ชฉํ‘œ์˜€์Šต๋‹ˆ๋‹ค.

  • ์ •์ฑ… ๋น„๊ต ๋ฐ ์„ ํƒ:
    1. ๋‹จ์ผ ์„ธ์…˜ + ์งง์€ ๋งŒ๋ฃŒ์‹œ๊ฐ„ (์ดˆ๊ธฐ์•ˆ): ๋ชจ๋ฐ”์ผ/์›น ๋™์‹œ ์ ‘์†์ด ๋ถˆ๊ฐ€๋Šฅํ•˜์—ฌ ์œ ์ € ๊ฒฝํ—˜(UX)์ด ์ €ํ•˜๋˜๊ณ  ์žฌ๋กœ๊ทธ์ธ ์ŠคํŠธ๋ ˆ์Šค๊ฐ€ ์ฆ๊ฐ€ํ•จ.
    2. ๋ฌด์ œํ•œ ์„ธ์…˜ (๋Œ€์•ˆ): ๋กœ๊ทธ์ธ ์œ ์ €๊ฐ€ ๋Š˜์–ด๋‚ ์ˆ˜๋ก ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ(RAM)์— ์„ธ์…˜ ๊ฐ์ฒด๊ฐ€ ๋ˆ„์ ๋˜์–ด OutOfMemory(OOM)๋กœ ์ธํ•œ ์„œ๋ฒ„ ๋‹ค์šด ์œ„ํ—˜ ๋ฐœ์ƒ.
    3. [์„ ํƒ] ๋‹ค์ค‘ ์„ธ์…˜ ์ง€์› + ์Šฌ๋ผ์ด๋”ฉ ์„ธ์…˜ + Remember-Me (30์ผ):
      • ๊ธฐ๋ณธ ์„ธ์…˜ 1์‹œ๊ฐ„: ํ™œ๋™ ์ค‘์ธ ์œ ์ €๋Š” ์„ธ์…˜ ๋งŒ๋ฃŒ์‹œ๊ฐ„์ด ์ง€์†์ ์œผ๋กœ ์—ฐ์žฅ(Sliding Session)๋˜์–ด ๋Š๊น€ ์—†๋Š” ์—ฐ์†์ ์ธ ๊ฒฝํ—˜ ์ œ๊ณต.
      • Remember-Me (30์ผ): ๋กœ๊ทธ์ธ ์‹œ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ์„ ํƒํ•œ ์œ ์ €์—๊ฒŒ 30์ผ ์žฅ๊ธฐ ์„ธ์…˜์„ ๋ถ€์—ฌํ•˜์—ฌ ์ด์šฉ ํŽธ๋ฆฌ์„ฑ ํ™•๋ณด.
      • ๋ฉ”๋ชจ๋ฆฌ ๋ณดํ˜ธ: 30์ผ ๋™์•ˆ ํ™œ๋™์ด ์ „ํ˜€ ์—†๋Š” ๋งŒ๋ฃŒ๋œ ์„ธ์…˜์€ ์„œ๋ธ”๋ฆฟ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ฆ‰์‹œ ํŒŒ๊ธฐ(Garbage Collection)ํ•˜๋„๋ก ๊ตฌ์„ฑํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๊ณ ๊ฐˆ ๋ฐฉ์ง€.

3. ์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ(Session Fixation Attack) ๋ฐฉ์–ด ๋ฉ”์ปค๋‹ˆ์ฆ˜

๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ๋‹จ์ˆœ ๊ธฐ๋ณธ ์„ธ์…˜ ๋ฐœ๊ธ‰๋งŒ ์ˆ˜ํ–‰ํ•˜๋ฉด, ๋กœ๊ทธ์ธ ์ „ ํ•ด์ปค๊ฐ€ ์œ ์ € ๋ธŒ๋ผ์šฐ์ €์— ๋ฏธ๋ฆฌ ์‹ฌ์–ด๋‘” ๋น„ํšŒ์› ์„ธ์…˜ ID๊ฐ€ ๋กœ๊ทธ์ธ ์‹œ ์œ ์ €์˜ ๊ถŒํ•œ ์„ธ์…˜์œผ๋กœ ๊ทธ๋Œ€๋กœ ์Šน๊ฒฉ๋˜์–ด ๊ณต๊ฒฉ์ž๊ฐ€ ์œ ์ €์˜ ๋กœ๊ทธ์ธ ๊ถŒํ•œ์„ ํ›”์ณ๋‚ด๋Š” ์„ธ์…˜ ํƒˆ์ทจ(Session Hijacking) ์œ„ํ˜‘์— ๋ฌด๋ฐฉ๋น„๋กœ ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

  • ํ•ด๊ฒฐ ๋ฐฉ์•ˆ: ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์งํ›„ Spring Security์˜ request.changeSessionId()๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
    ๊ธฐ์กด ์„ธ์…˜์— ๋ณด๊ด€๋˜์–ด ์žˆ๋˜ ์†์„ฑ ๋ฐ์ดํ„ฐ(์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋“ฑ)๋Š” ๋ฉ”๋ชจ๋ฆฌ ์ƒ์—์„œ 100% ๋ณด์กดํ•˜๋ฉด์„œ, ํด๋ผ์ด์–ธํŠธ ๋ธŒ๋ผ์šฐ์ €๋กœ ๋‚ด๋ ค์ฃผ๋Š” ์„ธ์…˜ ์‹๋ณ„์ž ํ‚ค(JSESSIONID)๋งŒ์„ 32์ž๋ฆฌ ์ƒˆ ๋‚œ์ˆ˜๋กœ ์ฆ‰์‹œ ์žฌ๋ฐœ๊ธ‰ํ•˜์—ฌ ๊ณต๊ฒฉ์ž์˜ ์ด์ „ ์„ธ์…˜ ID๋ฅผ ๋ฌผ๋ฆฌ์ ์œผ๋กœ ์™„์ „ํžˆ ๋ฌดํšจํ™”์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.

4. ๊ฒŒ์‹œํŒ(Post) ๋„๋ฉ”์ธ ์„ค๊ณ„ & ํ•ต์‹ฌ ๊ธฐ์ˆ ์  ์˜์‚ฌ๊ฒฐ์ • (Board Architecture & Decisions)

๊ฒŒ์‹œํŒ์€ Snowthing์—์„œ ๊ฐ€์žฅ ์ž์ฃผ ์ฝํžˆ๋Š” ๋„๋ฉ”์ธ์ด๋‹ค. ๊ทธ๋ž˜์„œ ๋‹จ์ˆœ CRUD๋กœ๋งŒ ๋งŒ๋“ค์ง€ ์•Š๊ณ , ๋ชฉ๋ก ์กฐํšŒ ๋น„์šฉ, ์ต๋ช… ๊ธ€ ๊ถŒํ•œ, ์‚ญ์ œ ์ •์ฑ…, ์ด๋ฏธ์ง€ ์ฒจ๋ถ€ ์ƒํƒœ๊นŒ์ง€ ๊ฐ™์ด ๋งž์ถฐ์„œ ์„ค๊ณ„ํ–ˆ๋‹ค.

1) ๊ฒŒ์‹œ๊ธ€ ๋„๋ฉ”์ธ ๊ตฌ์กฐ

๊ฒŒ์‹œ๊ธ€์˜ ์ค‘์‹ฌ ์—”ํ‹ฐํ‹ฐ๋Š” Post๋‹ค. Post ํ•˜๋‚˜๊ฐ€ ์ œ๋ชฉ, ๋ณธ๋ฌธ, ์ž‘์„ฑ IP, ์กฐํšŒ์ˆ˜, ๋Œ“๊ธ€ ์ˆ˜, ์ถ”์ฒœ/๋น„์ถ”์ฒœ ์ˆ˜, ์‚ญ์ œ ์—ฌ๋ถ€, ์ƒํƒœ๊ฐ’์„ ๊ฐ€์ง„๋‹ค.

์ฃผ๋ณ€ ์—”ํ‹ฐํ‹ฐ๋Š” ์—ญํ• ์„ ๋‚˜๋ˆ ์„œ ๋ถ™์˜€๋‹ค.

  • PostCategory: ์ž์œ ๊ฒŒ์‹œํŒ, ์ต๋ช… ๊ฒŒ์‹œํŒ, ์žฅ๋น„ Q&A, ๋ฆฌ์กฐํŠธ ๋ง›์ง‘ ๊ฐ™์€ ๊ฒŒ์‹œํŒ ์ข…๋ฅ˜๋ฅผ ๋‹ด๋‹นํ•œ๋‹ค.
  • PostImage: ๊ฒŒ์‹œ๊ธ€ ์ฒจ๋ถ€ ์ด๋ฏธ์ง€๋ฅผ 1:N์œผ๋กœ ์ €์žฅํ•œ๋‹ค. ์ด๋ฏธ์ง€๋Š” ์—ฌ๋Ÿฌ ์žฅ์ด ๊ฐ€๋Šฅํ•˜๊ณ  sortOrder๋กœ ํ‘œ์‹œ ์ˆœ์„œ๋ฅผ ๊ฐ€์ง„๋‹ค.
  • PostReaction: ์ถ”์ฒœ/๋น„์ถ”์ฒœ ์ด๋ ฅ์„ ์ €์žฅํ•œ๋‹ค. ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์˜ ์ถ”์ฒœ/๋น„์ถ”์ฒœ๊ณผ ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์˜ ์ต๋ช… ์ถ”์ฒœ/๋น„์ถ”์ฒœ์„ ๋‘˜ ๋‹ค ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ํšŒ์› ID ๋˜๋Š” anonymous_voter_id ์ฟ ํ‚ค ๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ๋ถ„ํ•œ๋‹ค.
  • Comment: ๊ฒŒ์‹œ๊ธ€ ํ•˜์œ„ ๋Œ“๊ธ€/๋Œ€๋Œ“๊ธ€์„ ๋‹ด๋‹นํ•œ๋‹ค.
  • Member: ๋กœ๊ทธ์ธ ํšŒ์›์ด ์ž‘์„ฑํ•œ ๊ฒŒ์‹œ๊ธ€์˜ ์ž‘์„ฑ์ž ์ •๋ณด๋‹ค.

์™ธ๋ถ€ API์—์„œ๋Š” DB PK์ธ post_id๋ฅผ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœํ•˜์ง€ ์•Š๊ณ  publicId๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ๋‚ด๋ถ€ PK๋Š” ์กฐ์ธ๊ณผ ์ธ๋ฑ์Šค์— ์“ฐ๊ณ , ์™ธ๋ถ€ ์‹๋ณ„์ž๋Š” URL๊ณผ API ์‘๋‹ต์— ์“ฐ๋Š” ์‹์œผ๋กœ ์ฑ…์ž„์„ ๋‚˜๋ˆด๋‹ค. ์ด๋ ‡๊ฒŒ ํ•ด๋‘๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ URL ์ˆซ์ž๋ฅผ ๋ฐ”๊ฟ”๊ฐ€๋ฉฐ ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜๋ฅผ ์ถ”์ธกํ•˜๋Š” ๋ฌธ์ œ๋„ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.

2) ํšŒ์›๊ณผ ๊ฒŒ์‹œ๊ธ€์˜ ์—ฐ๊ด€๊ด€๊ณ„

ํšŒ์›๊ณผ ๊ฒŒ์‹œ๊ธ€์€ Member 1 : N Post ๊ด€๊ณ„๋‹ค. ํ•œ ๋ช…์˜ ํšŒ์›์€ ์—ฌ๋Ÿฌ ๊ธ€์„ ์“ธ ์ˆ˜ ์žˆ๊ณ , ํ•˜๋‚˜์˜ ํšŒ์› ๊ธ€์€ ํ•œ ๋ช…์˜ ์ž‘์„ฑ์ž๋ฅผ ๊ฐ€์ง„๋‹ค.

์ฝ”๋“œ์—์„œ๋Š” Post.member๋ฅผ @ManyToOne(fetch = FetchType.LAZY)๋กœ ๋‘”๋‹ค. ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์„ ๋ณผ ๋•Œ๋งˆ๋‹ค ํšŒ์› ์—”ํ‹ฐํ‹ฐ ์ „์ฒด๊ฐ€ ํ•ญ์ƒ ํ•„์š”ํ•œ ๊ฒƒ์€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ๋ณธ์€ LAZY๋กœ ๋‘” ๊ฒƒ์ด๋‹ค. ํ•„์š”ํ•œ ํ™”๋ฉด์—์„œ๋Š” fetch join์ด๋‚˜ DTO projection์œผ๋กœ ํ•„์š”ํ•œ ์ž‘์„ฑ์ž ์ •๋ณด๋งŒ ๊ฐ€์ ธ์˜จ๋‹ค.

๋‹ค๋งŒ member_id๋Š” nullable์ด๋‹ค. ์ด์œ ๋Š” ์ต๋ช… ๊ฒŒ์‹œํŒ ๋•Œ๋ฌธ์ด๋‹ค.

  • ๋กœ๊ทธ์ธ ํšŒ์› ๊ธ€: member_id๊ฐ€ ์žˆ๋‹ค.
  • ๋กœ๊ทธ์ธ ํšŒ์›์ด ์ต๋ช…์œผ๋กœ ์“ด ๊ธ€: member_id๋ฅผ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๊ณ , ํ™”๋ฉด์—์„œ๋Š” ์ต๋ช…์œผ๋กœ ๋งˆ์Šคํ‚นํ•œ๋‹ค.
  • ๋น„๋กœ๊ทธ์ธ ์ต๋ช… ๊ธ€: member_id๊ฐ€ ์—†๋‹ค. ๋Œ€์‹  ์ž‘์„ฑ IP์™€ ์ต๋ช… ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ๋กœ ์ˆ˜์ •/์‚ญ์ œ ๊ถŒํ•œ์„ ๊ฒ€์ฆํ•œ๋‹ค.

์ด ๊ตฌ์กฐ์˜ ๋Œ€๊ฐ€๋Š” ๊ถŒํ•œ ๊ฒ€์ฆ์ด ์กฐ๊ธˆ ๋ณต์žกํ•ด์ง„๋‹ค๋Š” ์ ์ด๋‹ค. ๊ทธ๋ž˜์„œ ์ˆ˜์ •/์‚ญ์ œ ์‹œ ๋‹จ์ˆœํžˆ member_id๋งŒ ๋ณด๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ํšŒ์› ๊ธ€์ธ์ง€ ์ต๋ช… ๊ธ€์ธ์ง€ ๋จผ์ € ๋‚˜๋ˆˆ ๋’ค ๊ฐ๊ฐ ๋‹ค๋ฅธ ๊ฒ€์ฆ ๋กœ์ง์„ ํƒ„๋‹ค.

3) ๊ฒŒ์‹œ๊ธ€ ์ƒํƒœ์™€ ์œ ํ˜•

๊ฒŒ์‹œ๊ธ€ ์ƒํƒœ๋Š” PostStatus๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.

  • NORMAL: ์ •์ƒ ๊ธ€์ด๋‹ค. ๋ชฉ๋ก๊ณผ ์ƒ์„ธ ์กฐํšŒ ๋Œ€์ƒ์ด๋‹ค.
  • DELETED: ์ž‘์„ฑ์ž ๋˜๋Š” ๊ด€๋ฆฌ์ž์— ์˜ํ•ด ์‚ญ์ œ๋œ ๊ธ€์ด๋‹ค. ํ˜„์žฌ๋Š” Soft Delete ๊ฒฐ๊ณผ ์ƒํƒœ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
  • BLOCKED: ๊ด€๋ฆฌ์ž๊ฐ€ ๋ถ€์ ์ ˆํ•œ ๊ธ€์„ ์ฐจ๋‹จํ•œ ์ƒํƒœ๋‹ค.
  • HIDDEN: ๋น„๊ณต๊ฐœ๋‚˜ ์ˆจ๊น€ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ์ƒํƒœ๋ฅผ ์œ„ํ•ด ๋ถ„๋ฆฌํ•ด๋‘” ๊ฐ’์ด๋‹ค.
  • DRAFT: ์ž„์‹œ ์ €์žฅ์„ ๊ณ ๋ คํ•œ ์ƒํƒœ๊ฐ’์ด๋‹ค.

๊ฒŒ์‹œ๊ธ€ ์œ ํ˜•์€ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ผ ์นดํ…Œ๊ณ ๋ฆฌ๋‹ค. PostCategory.code๋กœ ๊ตฌ๋ถ„ํ•œ๋‹ค.

  • FREE: ์ž์œ ๊ฒŒ์‹œํŒ
  • ANONYMOUS: ์ต๋ช… ๊ฒŒ์‹œํŒ
  • QNA: ์žฅ๋น„ Q&A
  • FOOD: ๋ฆฌ์กฐํŠธ ๋ง›์ง‘

์ƒํƒœ์™€ ์œ ํ˜•์„ ์„ž์ง€ ์•Š์€ ์ด์œ ๋Š” ์—ญํ• ์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. NORMAL, DELETED๋Š” ๊ธ€์˜ ํ˜„์žฌ ์ฒ˜๋ฆฌ ์ƒํƒœ์ด๊ณ , FREE, ANONYMOUS๋Š” ๊ธ€์ด ์†ํ•œ ๊ฒŒ์‹œํŒ์ด๋‹ค. ์ด ๋‘˜์„ ๊ฐ™์€ enum์œผ๋กœ ํ•ฉ์น˜๋ฉด โ€œ์ต๋ช… ๊ฒŒ์‹œํŒ์— ์žˆ๋Š” ์‚ญ์ œ๋œ ๊ธ€โ€ ๊ฐ™์€ ์ƒํƒœ๋ฅผ ํ‘œํ˜„ํ•˜๊ธฐ๊ฐ€ ์ง€์ €๋ถ„ํ•ด์ง„๋‹ค.

4) ๋ชฉ๋ก/์ƒ์„ธ ์‘๋‹ต์„ ๋ถ„๋ฆฌํ•œ ์ด์œ 

๋ชฉ๋ก ์‘๋‹ต๊ณผ ์ƒ์„ธ ์‘๋‹ต์€ ์ผ๋ถ€๋Ÿฌ ๋‚˜๋ˆด๋‹ค.

๋ชฉ๋ก์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ธ€์„ ๊ณ ๋ฅด๋Š” ๋ฐ ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ์žˆ์œผ๋ฉด ๋œ๋‹ค. ๊ทธ๋ž˜์„œ PostListResponse์—๋Š” publicId, categoryName, title, writerNickname, thumbnailImageUrl, hasImage, viewCount, commentCount, likeCount, status, createdAt ๊ฐ™์€ ๋ชฉ๋ก์šฉ ํ•„๋“œ๋งŒ ๋‘”๋‹ค.

์ƒ์„ธ์—์„œ๋Š” ๋ณธ๋ฌธ๊ณผ ์ด๋ฏธ์ง€ ์ „์ฒด๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ๊ทธ๋ž˜์„œ PostDetailResponse์—๋Š” content, images, writer, isAnonymous ๊ฐ™์€ ์ƒ์„ธ ํ™”๋ฉด์šฉ ํ•„๋“œ๋ฅผ ํฌํ•จํ•œ๋‹ค.

์ด๋ ‡๊ฒŒ ๋‚˜๋ˆˆ ์ด์œ ๋Š” ๊ฐ„๋‹จํ•˜๋‹ค. ๋ชฉ๋ก 20๊ฐœ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š”๋ฐ ๋ณธ๋ฌธ ์ „์ฒด์™€ ์ด๋ฏธ์ง€ ๋ฐฐ์—ด๊นŒ์ง€ ๊ฐ™์ด ๊ฐ€์ ธ์˜ค๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ํด๋ฆญํ•˜์ง€๋„ ์•Š์€ ๊ธ€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋งค๋ฒˆ ์ฝ๊ณ  ๋‚ด๋ ค์ฃผ๊ฒŒ ๋œ๋‹ค. ๊ฒŒ์‹œ๊ธ€ ์ˆ˜๊ฐ€ ๋Š˜์ˆ˜๋ก DB I/O์™€ ์‘๋‹ต ํฌ๊ธฐ๊ฐ€ ๊ฐ™์ด ์ปค์ง„๋‹ค.

๊ทธ๋ž˜์„œ ๋ชฉ๋ก์€ ๊ฐ€๋ณ๊ฒŒ, ์ƒ์„ธ๋Š” ํ•„์š”ํ•œ ๋งŒํผ๋งŒ ๊ฐ€์ ธ์˜ค๊ฒŒ ํ–ˆ๋‹ค. ์ตœ๊ทผ ์ด๋ฏธ์ง€ ๋ฑƒ์ง€๋„ ๊ฐ™์€ ํ๋ฆ„์ด๋‹ค. ๋ชฉ๋ก์—์„œ ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ํ‘œ์‹œํ•˜๋ ค๊ณ  ๋งค๋ฒˆ post_image๋ฅผ ์กฐ์ธํ•˜์ง€ ์•Š๊ณ , Post.hasImage์— ์ฒจ๋ถ€ ์—ฌ๋ถ€๋ฅผ ์—ญ์ •๊ทœํ™”ํ•ด๋‘”๋‹ค. ๋Œ€์‹  ์ž‘์„ฑ/์ˆ˜์ • ์‹œ Post.replaceImages()์—์„œ ์ด๋ฏธ์ง€ ๋ชฉ๋ก๊ณผ hasImage๋ฅผ ๊ฐ™์ด ๋งž์ถฐ ๋ฐ์ดํ„ฐ๊ฐ€ ํ‹€์–ด์ง€์ง€ ์•Š๊ฒŒ ํ–ˆ๋‹ค.

5) ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ ์ •๋ ฌ ๊ธฐ์ค€

ํ˜„์žฌ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์€ ์›น ํ™”๋ฉด ๊ธฐ์ค€์œผ๋กœ offset pagination์„ ์ง€์›ํ•œ๋‹ค. page, size๋ฅผ ๋ฐ›์•„์„œ ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

์ •๋ ฌ ๊ธฐ์ค€์€ createdAt DESC, id DESC๋‹ค.

createdAt๋งŒ ์“ฐ์ง€ ์•Š๊ณ  id๋ฅผ ๊ฐ™์ด ๋‘” ์ด์œ ๋Š” ๊ฐ™์€ ์‹œ๊ฐ์— ์—ฌ๋Ÿฌ ๊ธ€์ด ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ •๋ ฌ ๊ธฐ์ค€์ด ํ•˜๋‚˜๋ฟ์ด๋ฉด ๊ฐ™์€ createdAt ๊ฐ’์„ ๊ฐ€์ง„ ๊ธ€๋“ค์˜ ์ˆœ์„œ๊ฐ€ DB ์‹คํ–‰ ๊ณ„ํš์— ๋”ฐ๋ผ ํ”๋“ค๋ฆด ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ตœ์‹  ์ž‘์„ฑ์ผ์„ ๋จผ์ € ๋ณด๊ณ , ๊ฐ™์€ ์‹œ๊ฐ„์ด๋ฉด ๋” ๋‚˜์ค‘์— ์ €์žฅ๋œ id๋ฅผ ํ•œ ๋ฒˆ ๋” ๊ธฐ์ค€์œผ๋กœ ๋‘”๋‹ค.

offset ๋ฐฉ์‹์€ ์›น ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ UI์™€ ์ž˜ ๋งž์ง€๋งŒ, ํŽ˜์ด์ง€๊ฐ€ ๋„ˆ๋ฌด ๊นŠ์–ด์ง€๋ฉด DB๊ฐ€ ์•ž์ชฝ ๋ฐ์ดํ„ฐ๋ฅผ ๋งŽ์ด ๊ฑด๋„ˆ๋›ฐ์–ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ MAX_OFFSET_PAGE ์ œํ•œ์„ ๋‘”๋‹ค.

๋ชจ๋ฐ”์ผ ๋ฌดํ•œ์Šคํฌ๋กค์ด๋‚˜ ๊นŠ์€ ํŽ˜์ด์ง€ ํƒ์ƒ‰์€ cursor pagination์ด ๋” ๋งž๋‹ค. ํ˜„์žฌ๋Š” cursor ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด cursor ๊ธฐ๋ฐ˜ ์กฐํšŒ๋กœ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ๊ณ , ๋‹ค์Œ ์ปค์„œ๋ฅผ ์‘๋‹ต์— ๋‚ด๋ ค์ฃผ๋Š” ๊ตฌ์กฐ๋ฅผ ์ค€๋น„ํ•ด๋’€๋‹ค.

6) ์ˆ˜์ • ๊ถŒํ•œ ๊ฒ€์ฆ ๋ฐฉ์‹

๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •์€ ์ž‘์„ฑ์ž ๋ณธ์ธ๋งŒ ๊ฐ€๋Šฅํ•˜๊ฒŒ ์žก์•˜๋‹ค. ๊ด€๋ฆฌ์ž๋Š” ํƒ€์ธ์˜ ๊ธ€์„ ์ˆ˜์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ด€๋ฆฌ์ž๊ฐ€ ๋ณธ๋ฌธ์„ ์ง์ ‘ ๋ฐ”๊พธ๋ฉด ์› ์ž‘์„ฑ์ž์˜ ์˜๋„์™€ ์ฑ…์ž„ ์†Œ์žฌ๊ฐ€ ์„ž์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ด€๋ฆฌ์ž๋Š” ์‚ญ์ œ๋‚˜ ์ฐจ๋‹จ ๊ฐ™์€ ์šด์˜ ์กฐ์น˜๋งŒ ํ•˜๋Š” ์ชฝ์ด ๋” ๋ช…ํ™•ํ•˜๋‹ค.

๊ฒ€์ฆ ํ๋ฆ„์€ ๊ธ€ ์œ ํ˜•์— ๋”ฐ๋ผ ๋‚˜๋‰œ๋‹ค.

  • ํšŒ์› ๊ธ€: ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ publicId์™€ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž์˜ publicId๊ฐ€ ๊ฐ™์•„์•ผ ํ•œ๋‹ค.
  • ๋กœ๊ทธ์ธ ํšŒ์›์ด ์ž‘์„ฑํ•œ ์ต๋ช… ๊ธ€: ํ™”๋ฉด์—๋Š” ์ต๋ช…์œผ๋กœ ๋ณด์ด์ง€๋งŒ ์„œ๋ฒ„์—๋Š” ์ž‘์„ฑ์ž ํšŒ์› ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ, ๊ฐ™์€ ํšŒ์›์ด๋ฉด ๋น„๋ฐ€๋ฒˆํ˜ธ ์—†์ด ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋น„๋กœ๊ทธ์ธ ์ต๋ช… ๊ธ€: ์ž‘์„ฑ ๋‹น์‹œ ์ž…๋ ฅํ•œ anonymousPassword๋ฅผ BCrypt๋กœ ๋น„๊ตํ•œ๋‹ค.

์ด ๊ฒ€์ฆ์€ ํ”„๋ก ํŠธ์—์„œ ๋ฒ„ํŠผ์„ ์ˆจ๊ธฐ๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ๋๋‚ด์ง€ ์•Š๋Š”๋‹ค. ํ”„๋ก ํŠธ ๊ฐ€๋“œ๋Š” UX์šฉ์ด๊ณ , ์‹ค์ œ ๋ณด์•ˆ์€ PostService.validateEditPermission()์—์„œ ํ•œ ๋ฒˆ ๋” ๋ง‰๋Š”๋‹ค. URL์„ ์ง์ ‘ ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ API๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•ด๋„ ์„œ๋ฒ„์—์„œ 403์œผ๋กœ ๋ง‰ํ˜€์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

7) Hard Delete์™€ Soft Delete ๋น„๊ต

Hard Delete๋Š” DB row๋ฅผ ์‹ค์ œ๋กœ ์‚ญ์ œํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

DELETE FROM post WHERE post_id = ?

์žฅ์ ์€ ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚จ์ง€ ์•Š์•„ ๊ตฌ์กฐ๊ฐ€ ๋‹จ์ˆœํ•˜๋‹ค๋Š” ์ ์ด๋‹ค. ํ•˜์ง€๋งŒ ํ•œ ๋ฒˆ ์ง€์šฐ๋ฉด ๋ณต๊ตฌ๊ฐ€ ์–ด๋ ต๊ณ , ๋Œ“๊ธ€/์ถ”์ฒœ/์ด๋ฏธ์ง€ ๊ฐ™์€ ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ๋„ ๊ฐ™์ด ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ๊ฒฐ์ •ํ•ด์•ผ ํ•œ๋‹ค. ์‹ ๊ณ ๋‚˜ ๋ถ„์Ÿ์ด ์ƒ๊ฒผ์„ ๋•Œ โ€œ๋ฌด์Šจ ๊ธ€์ด ์žˆ์—ˆ๋Š”์ง€โ€ ํ™•์ธํ•˜๊ธฐ๋„ ์–ด๋ ต๋‹ค.

Soft Delete๋Š” row๋ฅผ ์ง€์šฐ์ง€ ์•Š๊ณ  ์‚ญ์ œ ํ‘œ์‹œ๋งŒ ๋‚จ๊ธฐ๋Š” ๋ฐฉ์‹์ด๋‹ค.

UPDATE post
SET is_deleted = true,
    status = 'DELETED',
    deleted_at = NOW()
WHERE post_id = ?

์žฅ์ ์€ ์ด๋ ฅ์ด ๋‚จ๋Š”๋‹ค๋Š” ์ ์ด๋‹ค. ๋Œ€์‹  ๋ชจ๋“  ์กฐํšŒ์—์„œ ์‚ญ์ œ๋œ ๊ธ€์„ ์ œ์™ธํ•ด์•ผ ํ•˜๊ณ , unique ์ œ์•ฝ์ด๋‚˜ ์ธ๋ฑ์Šค ์„ค๊ณ„๋„ ์‚ญ์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.

8) ํ˜„์žฌ ์‚ญ์ œ ๋ฐฉ์‹์„ ์„ ํƒํ•œ ์ด์œ 

ํ˜„์žฌ๋Š” Soft Delete๋ฅผ ์„ ํƒํ–ˆ๋‹ค. ์ด์œ ๋Š” ๊ฒŒ์‹œํŒ ์„œ๋น„์Šค์—์„œ ์‚ญ์ œ๊ฐ€ ๋‹จ์ˆœํ•œ ์ €์žฅ๊ณต๊ฐ„ ์ •๋ฆฌ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ฒซ ๋ฒˆ์งธ ์ด์œ ๋Š” ์šด์˜ ์ด๋ ฅ์ด๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋ฌธ์ œ ์žˆ๋Š” ๊ธ€์„ ์˜ฌ๋ฆฐ ๋’ค ๋ฐ”๋กœ ์‚ญ์ œํ•ด๋ฒ„๋ฆฌ๋ฉด, ๊ด€๋ฆฌ์ž ์ž…์žฅ์—์„œ๋Š” ์–ด๋–ค ์ผ์ด ์žˆ์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์–ด๋ ต๋‹ค. Soft Delete๋Š” ๊ธ€์„ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ์ˆจ๊ธฐ๋ฉด์„œ๋„ ์„œ๋ฒ„์—๋Š” ์ด๋ ฅ์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๋‹ค.

๋‘ ๋ฒˆ์งธ ์ด์œ ๋Š” ๋Œ“๊ธ€๊ณผ ์ถ”์ฒœ ๊ฐ™์€ ์ฃผ๋ณ€ ๋ฐ์ดํ„ฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ฒŒ์‹œ๊ธ€์„ Hard Deleteํ•˜๋ฉด ๋Œ“๊ธ€, ์ด๋ฏธ์ง€, ์ถ”์ฒœ ๋ฐ์ดํ„ฐ๋„ ๊ฐ™์ด ์‚ญ์ œํ• ์ง€ ๋‚จ๊ธธ์ง€ ๊ฒฐ์ •ํ•ด์•ผ ํ•œ๋‹ค. ์ง€๊ธˆ ๋‹จ๊ณ„์—์„œ๋Š” ์‹ค์ œ row๋ฅผ ์ œ๊ฑฐํ•˜๊ธฐ๋ณด๋‹ค isDeleted, status, deletedAt์œผ๋กœ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๋Š” ์ชฝ์ด ๋” ์•ˆ์ „ํ•˜๋‹ค.

์„ธ ๋ฒˆ์งธ ์ด์œ ๋Š” UX๋‹ค. ๋ชฉ๋ก์—์„œ ๋ฐฉ๊ธˆ ๋ณด์ด๋˜ ๊ธ€์ด ์‚ญ์ œ๋˜์—ˆ์„ ๋•Œ ์™„์ „ํžˆ ์‚ฌ๋ผ์ง€๋Š” ๊ฒƒ๋ณด๋‹ค, ํ•„์š”ํ•˜๋ฉด โ€œ์‚ญ์ œ๋œ ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹คโ€์ฒ˜๋Ÿผ ์ƒํƒœ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ์ชฝ์ด ํ๋ฆ„์„ ์„ค๋ช…ํ•˜๊ธฐ ์‰ฝ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„์€ @SQLDelete์™€ @SQLRestriction("is_deleted = false")๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ์‚ญ์ œ ํ˜ธ์ถœ์€ update๋กœ ๋ฐ”๋€Œ๊ณ , ๊ธฐ๋ณธ ์กฐํšŒ์—์„œ๋Š” ์‚ญ์ œ๋œ ๊ธ€์ด ๋น ์ง„๋‹ค.

9) ์‹คํ–‰ ์ฟผ๋ฆฌ ํ™•์ธ ๊ฒฐ๊ณผ

์ƒ์„ธ ์กฐํšŒ๋Š” PostRepository.findWithMemberAndCategoryByPublicId()๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

@Query("SELECT p FROM Post p LEFT JOIN FETCH p.member JOIN FETCH p.category WHERE p.publicId = :publicId")
Optional<Post> findWithMemberAndCategoryByPublicId(@Param("publicId") String publicId);

member๋Š” ๋น„๋กœ๊ทธ์ธ ์ต๋ช… ๊ธ€์—์„œ ์—†์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ LEFT JOIN FETCH๋ฅผ ์“ด๋‹ค. category๋Š” ๊ฒŒ์‹œ๊ธ€์— ๋ฐ˜๋“œ์‹œ ์žˆ์–ด์•ผ ํ•˜๋ฏ€๋กœ JOIN FETCH๋ฅผ ์“ด๋‹ค.

์ด ์ฟผ๋ฆฌ๋กœ ์ƒ์„ธ ์กฐํšŒ ์‹œ ๊ฒŒ์‹œ๊ธ€, ์ž‘์„ฑ์ž, ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜จ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์„œ๋น„์Šค๋‚˜ DTO ๋ณ€ํ™˜ ๊ณผ์ •์—์„œ post.getMember(), post.getCategory()๋ฅผ ์ ‘๊ทผํ•  ๋•Œ ์ถ”๊ฐ€ select๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ชฉ๋ก ์กฐํšŒ๋„ ์ž‘์„ฑ์ž์™€ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๊ฐ™์ด ๋ณด์—ฌ์ค˜์•ผ ํ•˜๋ฏ€๋กœ fetch join์„ ์ ์šฉํ•œ๋‹ค.

@Query("SELECT p FROM Post p LEFT JOIN FETCH p.member JOIN FETCH p.category WHERE p.category.code = :categoryCode")
Page<Post> findByCategoryCodeWithMemberAndCategory(@Param("categoryCode") String categoryCode, Pageable pageable);

์ด๋ฏธ์ง€๋Š” ๋ชฉ๋ก ์กฐํšŒ์—์„œ ์กฐ์ธํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ชฉ๋ก์—๋Š” ์ „์ฒด ์ด๋ฏธ์ง€ ๋ฐฐ์—ด์ด ํ•„์š”ํ•˜์ง€ ์•Š๊ณ , ์ด๋ฏธ์ง€ ์กด์žฌ ์—ฌ๋ถ€๋งŒ ํ•„์š”ํ•˜๋‹ค. ๊ทธ๋ž˜์„œ hasImage๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ด ์„ ํƒ์€ ์ฝ๊ธฐ ์„ฑ๋Šฅ์„ ์–ป๋Š” ๋Œ€์‹ , ์ž‘์„ฑ/์ˆ˜์ • ์‹œ hasImage๋ฅผ ๋ฐ˜๋“œ์‹œ ๊ฐ™์ด ๊ฐฑ์‹ ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๋Œ€๊ฐ€๊ฐ€ ์žˆ๋‹ค. ๊ทธ ๋ถ€๋ถ„์€ Post.replaceImages()๋กœ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค.

10) ์ฃผ์š” ์˜ˆ์™ธ ์‘๋‹ต

๊ฒŒ์‹œ๊ธ€ ๋„๋ฉ”์ธ์—์„œ ์ฃผ๋กœ ๋‚ด๋ ค๊ฐ€๋Š” ์˜ˆ์™ธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • POST_NOT_FOUND (404): ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ธ€, ์‚ญ์ œ๋œ ๊ธ€, ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณผ ์ˆ˜ ์—†๋Š” ๊ธ€์— ์ ‘๊ทผํ•œ ๊ฒฝ์šฐ
  • POST_CATEGORY_NOT_FOUND (404): ์š”์ฒญํ•œ categoryCode์— ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œํŒ์ด ์—†๋Š” ๊ฒฝ์šฐ
  • ACCESS_DENIED (403): ์ž‘์„ฑ์ž๊ฐ€ ์•„๋‹Œ ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜์ •/์‚ญ์ œ๋ฅผ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ
  • INVALID_ANON_PASSWORD (403): ์ต๋ช… ๊ธ€ ์ˆ˜์ •/์‚ญ์ œ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‹€๋ฆฐ ๊ฒฝ์šฐ
  • INVALID_CREDENTIALS (401): ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ์ผ๋ฐ˜ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์—์„œ ์ธ์ฆ ์ •๋ณด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ
  • INVALID_INPUT (400): ์ต๋ช… ๊ธ€ ์ž‘์„ฑ/์ถ”์ฒœยท๋น„์ถ”์ฒœ ๋“ฑ์—์„œ ํ•„์š”ํ•œ ๊ฐ’์ด ๋น ์ง„ ๊ฒฝ์šฐ
  • INVALID_PAGE_SIZE (400): ํ—ˆ์šฉ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚œ size ์š”์ฒญ
  • INVALID_PAGE_LIMIT (400): offset page ์ œํ•œ์„ ๋„˜๊ธด ์š”์ฒญ

11) CSRF

๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ ๊ฐ™์€ CUD ์š”์ฒญ์€ CSRF ๊ณต๊ฒฉ ํ‘œ์ ์ด ๋˜๊ธฐ ์‰ฝ๋‹ค. Spring Security์˜ CookieCsrfTokenRepository.withHttpOnlyFalse()๋ฅผ ์ ์šฉํ–ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ Double Submit Cookie ํŒจํ„ด์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค. ๋ฐฑ์—”๋“œ๊ฐ€ XSRF-TOKEN ์ฟ ํ‚ค๋ฅผ ๋ฐœ๊ธ‰ํ•˜๋ฉด, ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ์ž์› ๋ณ€๊ฒฝ ์š”์ฒญ(POST, PUT, DELETE)์„ ๋ณด๋‚ผ ๋•Œ ์ฟ ํ‚ค ๊ฐ’์„ ์ฝ์–ด X-XSRF-TOKEN HTTP ํ—ค๋”์— ๋‹ด์•„์„œ ๋ณด๋‚ธ๋‹ค. ์„œ๋ฒ„์˜ CsrfFilter๋Š” ์ฟ ํ‚ค์˜ ํ† ํฐ ๊ฐ’๊ณผ ํ—ค๋”์˜ ํ† ํฐ ๊ฐ’์ด ์ผ์น˜ํ•˜๋Š”์ง€ ๋น„๊ตํ•˜์—ฌ ๊ฒ€์ฆํ•œ๋‹ค. ์™ธ๋ถ€ ํ•ดํ‚น ์‚ฌ์ดํŠธ๋Š” ๋™์ผ ์ถœ์ฒ˜ ์ •์ฑ…(SOP) ์ œ์•ฝ์œผ๋กœ ์ธํ•ด ์‚ฌ์šฉ์ž์˜ XSRF-TOKEN ์ฟ ํ‚ค๋ฅผ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์ฝ์„ ์ˆ˜ ์—†์–ด X-XSRF-TOKEN ํ—ค๋”๋ฅผ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ•˜๋ฏ€๋กœ ์œ„์กฐ๋œ ์š”์ฒญ์€ 403 Forbidden์œผ๋กœ ์ฐจ๋‹จ๋œ๋‹ค.

5. ํ”„๋กœ์ ํŠธ ๋ฌผ๋ฆฌ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ (Project Structure)

snowthing/ (ํ”„๋กœ์ ํŠธ ์ตœ์ƒ์œ„ ๋ฃจํŠธ)
โ”œโ”€โ”€ backend/                  # Spring Boot ๋ฐฑ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
โ”‚   โ”œโ”€โ”€ src/main/java/com/ikae/snowthing/
โ”‚   โ”‚   โ”œโ”€โ”€ domain/auth/      # ์„ธ์…˜ ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ์ปจํŠธ๋กค๋Ÿฌ & ์„œ๋น„์Šค
โ”‚   โ”‚   โ”œโ”€โ”€ domain/member/    # ํšŒ์›, N:M ์Šคํ‚ค์žฅ/์„ฑํ–ฅ ์—”ํ‹ฐํ‹ฐ & ๋ ˆํฌ์ง€ํ† ๋ฆฌ
โ”‚   โ”‚   โ””โ”€โ”€ global/config/    # SecurityConfig, CorsConfig, DataInitializer
โ”‚   โ””โ”€โ”€ src/test/java/        # 25๊ฐœ ๋ฐฑ์—”๋“œ ๋‹จ์œ„/ํ†ตํ•ฉ/๋ณด์•ˆ ํ…Œ์ŠคํŠธ ์ˆ˜ํŠธ
โ”œโ”€โ”€ frontend/                 # Next.js ํ”„๋ก ํŠธ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
โ”‚   โ”œโ”€โ”€ app/signup/page.tsx   # ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ •๊ทœ์‹ & N:M ์ฒดํฌ๋ฐ•์Šค ํšŒ์›๊ฐ€์ž… UI
โ”‚   โ”œโ”€โ”€ app/login/page.tsx    # credentials: 'include' ์„ธ์…˜ ๋กœ๊ทธ์ธ UI
โ”‚   โ””โ”€โ”€ app/page.tsx          # ๋ฉ”์ธ ํ”„๋กœํ•„ ๋Œ€์‹œ๋ณด๋“œ & ํ”„๋กœํ•„ ์ˆ˜์ • UI (PUT)
โ”œโ”€โ”€ docs/                     # ๊ธฐํš, ์•„ํ‚คํ…์ฒ˜, ERD, API ๋ช…์„ธ ๋ฌธ์„œ
โ”‚   โ”œโ”€โ”€ conception/           # ๊ธฐํš ๋ฐ ์•„ํ‚คํ…์ฒ˜ ๋ช…์„ธ์„œ (sprint01/)
โ”‚   โ””โ”€โ”€ project/              # work.md ์ž‘์—… ์ด๋ ฅ ๋ฐ sql_logs ๋ฆฌํฌํŠธ
โ””โ”€โ”€ README.md

6. ์‹คํ–‰ ๋ฐ ํ…Œ์ŠคํŠธ (Build & Run)

Backend (Spring Boot)

cd backend
.\gradlew.bat test       # ์ „์ฒด 25๊ฐœ ํ†ตํ•ฉ/๋‹จ์› ํ…Œ์ŠคํŠธ ์‹คํ–‰
.\gradlew.bat bootRun    # 8080 ํฌํŠธ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„ ๊ธฐ๋™

Frontend (Next.js)

cd frontend
npm run lint             # ์ฝ”๋“œ ์Šคํƒ€์ผ ๊ฒ€์ฆ
npm run build            # ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ
npm run dev              # 3000 ํฌํŠธ ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๊ธฐ๋™

About

community for winter sports

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages