μΉμΊ μ μ΄μ©ν 4μ»· μ¬μ§ 촬μ μλΉμ€
- π₯ μΉμΊ 촬μ: μ€μκ° μΉμΊ μΌλ‘ 4μ₯μ μ¬μ§ μ°μ 촬μ
- β±οΈ μΉ΄μ΄νΈλ€μ΄: 5μ΄ μΉ΄μ΄νΈλ€μ΄μΌλ‘ 촬μ μ€λΉ μκ° μ 곡
- π¨ λ€μν νν°: 11κ°μ§ κ°μ± νν° μ 곡
- Original, Clearly, Monochrome, Sepia, Warm, Cool
- Vintage, Fade, Milk, Film, Retro
- πΌοΈ 4μ»· ν©μ±: 촬μν 4μ₯μ μ¬μ§μ μλμΌλ‘ μΈλ‘ν μ€νΈλ¦½μΌλ‘ ν©μ±
- πΎ μ¬μ§ μ μ₯: μμ±λ 4μ»· μ¬μ§μ PNG νμμΌλ‘ λ€μ΄λ‘λ
- π λ€μ μ°κΈ°: λ§μμ λ€μ§ μμΌλ©΄ μΈμ λ λ€μ 촬μ κ°λ₯
- Java 17
- Spring Boot 3.3.5
- Gradle 9.2.1
- HTML5 / CSS3
- Vanilla JavaScript
- Canvas API
- MediaStream API
- Spring Boot Web
- Spring Boot Validation
- Lombok
- Spring Boot DevTools
clicksnap/
βββ src/
β βββ main/
β β βββ java/com/clicksnap/
β β β βββ config/
β β β β βββ WebConfig.java # CORS λ° μ μ 리μμ€ μ€μ
β β β βββ controller/
β β β β βββ PhotoController.java # μ¬μ§ μ
λ‘λ API
β β β βββ dto/
β β β β βββ PhotoResponse.java # μλ΅ DTO
β β β βββ service/
β β β β βββ PhotoService.java # μ¬μ§ μ²λ¦¬ λΉμ¦λμ€ λ‘μ§
β β β β βββ StorageService.java # νμΌ μ μ₯ μΈν°νμ΄μ€
β β β β βββ FileSystemStorageService.java # νμΌ μμ€ν
μ μ₯ ꡬν체
β β β βββ ClicksnapApplication.java # λ©μΈ μ ν리μΌμ΄μ
β β βββ resources/
β β βββ static/
β β β βββ css/style.css # μ€νμΌμνΈ
β β β βββ js/script.js # λ©μΈ JavaScript
β β β βββ index.html # λ©μΈ νμ΄μ§
β β βββ application.yml # μ ν리μΌμ΄μ
μ€μ
β βββ test/
β βββ java/com/clicksnap/
β βββ ClicksnapApplicationTests.java
βββ build.gradle
βββ README.md
- Java 17 μ΄μ
- Gradle 9.2.1 μ΄μ
- μΉμΊ μ΄ μλ νκ²½
git clone [repository-url]
cd clicksnapsrc/main/resources/ λλ ν 리μ application-dev.yml νμΌμ μμ±νμΈμ:
file:
upload-dir: ./uploads # νμΌ μ μ₯ κ²½λ‘# Gradleμ μ΄μ©ν λΉλ
./gradlew build
# μ ν리μΌμ΄μ
μ€ν
./gradlew bootRunhttp://localhost:8080
μ¬μ§ νμΌμ μλ²μ μ λ‘λν©λλ€.
Endpoint
POST /api/photos
Request
- Content-Type:
multipart/form-data - Parameter:
image(MultipartFile)
Response
{
"accessUrl": "/images/550e8400-e29b-41d4-a716-446655440000.png"
}HTTP Status
201 Created: μ λ‘λ μ±κ³΅400 Bad Request: μλͺ»λ μμ²
μΉ΄λ©λΌ κΆν μμ² β 촬μ μμ β 5μ΄ μΉ΄μ΄νΈλ€μ΄ β 1μ»· 촬μ
β 5μ΄ μΉ΄μ΄νΈλ€μ΄ β 2μ»· 촬μ β ... β 4μ»· μλ£ β μλ ν©μ±
- λ¨μΌ μ»·: 900 x 1200px (3:4 λΉμ¨)
- μ΅μ’ μ€νΈλ¦½: 1000 x 5200px (μΈλ‘ν)
- μ¬μ§ μμ: 900 x 4800px (4μ»·)
- νλ μ μ¬λ°±: μ/ν/μ’/μ° 50px
- ꡬλΆμ : 45px (κ²μ μ)
- Canvas Filter API μ§μ λΈλΌμ°μ μμλ CSS Filter λ¬Έλ² μ¬μ©
- λ―Έμ§μ λΈλΌμ°μ μμλ ν½μ λ¨μ νν°λ§ fallback μ 곡
- λͺ¨λ μ£Όμ λΈλΌμ°μ νΈν (Chrome, Safari, Firefox, Edge)
- μ μ₯ κ²½λ‘:
./uploads(μ€μ κ°λ₯) - νμΌλͺ : UUID κΈ°λ° μ€λ³΅ λ°©μ§
- 보μ: Path Traversal 곡격 λ°©μ§
- μ©λ μ ν: μ΅λ 10MB
spring:
servlet:
multipart:
max-file-size: 10MB # λ¨μΌ νμΌ μ΅λ ν¬κΈ°
max-request-size: 10MB # μ 체 μμ² μ΅λ ν¬κΈ°
file:
upload-dir: ./uploads # μ
λ‘λ λλ ν 리
access-url-prefix: /images/ # νμΌ μ κ·Ό URL μ λμ¬.allowedOrigins("http://localhost:3000", "http://127.0.0.1:5500")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")src/main/resources/static/js/script.jsμμ λ€μ κ°μ μμ ν μ μμ΅λλ€:
const PHOTO_COUNT = 4; // 촬μ λ§€μ (κΈ°λ³Έ: 4μ»·)
const COUNTDOWN_SEC = 5; // μΉ΄μ΄νΈλ€μ΄ μκ° (κΈ°λ³Έ: 5μ΄)
const STRIP_BG_COLOR = "#040404ff"; // νλ μ μμwindow.CLICKSNAP_FILTERS = {
// ... κΈ°μ‘΄ νν°λ€
custom: {
name: "Custom",
filter: "brightness(1.2) contrast(1.1)"
}
};- λΈλΌμ°μ μμ μΉ΄λ©λΌ κΆνμ νμ©νλμ§ νμΈ
- HTTPS νκ²½μ΄ μλ κ²½μ°
localhostμμλ§ μλ
application.ymlμμmax-file-sizeμ€μ νμΈ
WebConfig.javaμallowedOriginsμ νλ‘ νΈμλ μ£Όμ μΆκ°
- Group: com.clicksnap
- Artifact: clicksnap
- Version: 0.0.1-SNAPSHOT
- Build File: click-snap-2.0.1.jar
μ΄ νλ‘μ νΈλ [λΌμ΄μΌμ€ λͺ μ] νμ λ°°ν¬λ©λλ€.