diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 29636ed..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/README.md b/README.md index 8ea86cc..c607680 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,190 @@ -# dev-queue -A personal dev article & video queue with vocabulary tracking, built while learning Java/Spring Boot +# ๐Ÿ“š DevQueue + +๊ฐœ๋ฐœ ๋ฌธ์„œ/์˜์ƒ์„ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์ €์žฅํ•˜๊ณ , ๋ชจ๋ฅด๋Š” ๋‹จ์–ด๋ฅผ ๋“ฑ๋กํ•ด ๋งค์ผ ๋žœ๋ค์œผ๋กœ ๋ณต์Šตํ•˜๋Š” ๊ฐœ์ธ์šฉ ์ฝ์„๊ฑฐ๋ฆฌ ํ + ๋‹จ์–ด์žฅ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.
+A personal reading-queue and vocabulary app that saves dev articles/videos by category, and lets you review random unknown words once a day. + +**๐Ÿ”— ๋ฐฐํฌ ๋งํฌ / Live demo: [devqueue.duckdns.org](http://devqueue.duckdns.org)** + + + + + + + + + + +
+ +> Java/Spring Boot๋ฅผ ํ•™์Šตํ•˜๋ฉฐ ์ง„ํ–‰ํ•œ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. RPI(Researchโ€“Planโ€“Implement) ์›Œํฌํ”Œ๋กœ๋กœ AI์™€ ํ•จ๊ป˜ ์„ค๊ณ„ํ•˜๊ณ , ๋ชจ๋“  ๊ตฌํ˜„ ์ฝ”๋“œ๋Š” ์ง์ ‘ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
+> A personal project built while learning Java/Spring Boot. Designed with AI using the RPI (Researchโ€“Planโ€“Implement) workflow; all implementation code was written by hand. + +--- + +## ๐Ÿ’ก ์™œ ๋งŒ๋“ค์—ˆ๋‚˜ / Why I built this + +ํ‰์†Œ ์œ ์šฉํ•œ ๊ฐœ๋ฐœ ๋ฌธ์„œ๋‚˜ ์˜์ƒ์„ ์ €์žฅํ•ด๋‘๊ณ  ์ฝ๊ฑฐ๋‚˜ ์‹œ์ฒญํ•˜๋ฉด์„œ ๊ธฐ์ˆ  ๊ณต๋ถ€์™€ ์˜์–ด ๊ณต๋ถ€๋ฅผ ํ•จ๊ป˜ ํ•˜๋Š” ๊ฒƒ์„ ์ข‹์•„ํ•ฉ๋‹ˆ๋‹ค.
+๊ทธ๋Ÿฐ๋ฐ ๋งํฌ๋ฅผ ๊ณ„์† ์—ฌ๋Ÿฌ ๊ตฐ๋ฐ ๋ฉ”๋ชจํ•ด๋‘๋‹ค ๋ณด๋‹ˆ ํ•„์š”ํ•œ ๋งํฌ๋ฅผ ์ฐพ๊ธฐ๊ฐ€ ์–ด๋ ต๊ฑฐ๋‚˜ ์ €์žฅํ•ด๋‘๊ณ  ์žŠ์–ด๋ฒ„๋ฆด ๋•Œ๊ฐ€ ๋งŽ์•˜์Šต๋‹ˆ๋‹ค. + +Java + Spring ๊ณต๋ถ€๋ฅผ ํ•˜๋ฉด์„œ ์‹ค์Šตํ•  ๊ฒธ, ํ‰์†Œ์— ํ•„์š”ํ–ˆ๋˜ ์ž‘์€ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๋ฉฐ ๊ณต๋ถ€ํ•ด๋ณด๋ฉด ์–ด๋–จ๊นŒ ์ƒ๊ฐํ•ด์„œ ์‹œ์ž‘ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ๋งํฌ๋ฅผ ์ •๋ฆฌํ•ด์„œ ์ €์žฅํ•˜๊ณ , ์ฝ์œผ๋ฉด์„œ ๋ชจ๋ฅด๋Š” ๋‹จ์–ด๋Š” ๋ฐ”๋กœ ๊ธฐ๋กํ•ด๋’€๋‹ค๊ฐ€, ๋งค์ผ ํŒ์—…์„ ๋„์›Œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ณต์Šตํ•˜๋Š” ๊ฐœ์ธ์šฉ ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. + +Java/Spring ๊ธฐ์ดˆ๋Š” ๊น€์˜ํ•œ ๋‹˜์˜ ์Šคํ”„๋ง ๊ฐ•์˜๋กœ ๋‹ค์กŒ๊ณ , ์ดํ›„ RPI ์›Œํฌํ”Œ๋กœ ์„ค๊ณ„ยท์ธ์ฆยทJPA ์ „ํ™˜ยทAWS ๋ฐฐํฌยทCI/CD๋Š” ์ง์ ‘ ํ™•์žฅํ–ˆ์Šต๋‹ˆ๋‹ค. + +I like saving useful dev articles and videos to read or watch, combining tech learning with English study. But scattering links across multiple notes made them hard to find, and I often forgot I'd even saved them. + +While studying Java + Spring, I thought it'd be a good way to practice by building a small tool I actually needed. + +So I built a personal tool that organizes links by category, lets me jot down unknown words while reading, and surfaces a daily popup to review them naturally. + +Java/Spring fundamentals were built through Kim Young-han's Spring course; everything beyond that โ€” RPI workflow design, authentication, JPA migration, AWS deployment, and CI/CD โ€” was extended independently. + +--- + +## ๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ / Tech Stack + +**Backend** +- Java 17, Spring Boot 4.1.1, Gradle +- Spring MVC, Thymeleaf +- Spring Data JPA, MySQL 8 +- Spring Security Crypto (BCrypt) + +**Infra / DevOps** +- AWS EC2 (Amazon Linux 2023), AWS RDS (MySQL) +- GitHub Actions (Checkstyle + SpotBugs) +- DuckDNS + +**Dev Process** +- RPI (Researchโ€“Planโ€“Implement) workflow +- Git branching strategy โ€” one feature branch + PR per Phase + +--- + +## ๐ŸŽฎ ์ฃผ์š” ๊ธฐ๋Šฅ / Key Features + +| ํ•œ๊ธ€ | English | +|---|---| +| ํšŒ์›๊ฐ€์ž… / ๋กœ๊ทธ์ธ (์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ, BCrypt ์•”ํ˜ธํ™”) | Sign up / log in (session-based auth, BCrypt hashing) | +| ๊ฐœ๋ฐœ ๋ฌธ์„œ/์˜์ƒ ๋งํฌ ์ €์žฅ (์ œ๋ชฉยทURLยท์นดํ…Œ๊ณ ๋ฆฌยท๋ฉ”๋ชจ) | Save dev article/video links (title, URL, category, memo) | +| ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ๋ง (๊ฐœ๋ฐœ๋ฌธ์„œ / AI / ์ฑ„์šฉ์ •๋ณด / ๊ธฐํƒ€) | Filter by category (Dev Docs / AI / Jobs / Etc) | +| ๋ฌธ์„œ๋ณ„ ๋ชจ๋ฅด๋Š” ๋‹จ์–ด ๋“ฑ๋กยท์‚ญ์ œยท์•”๊ธฐ ์—ฌ๋ถ€ ํ† ๊ธ€ | Add/delete words per article, toggle memorized status | +| ๋งค์ผ ์ฒ˜์Œ ์ ‘์† ์‹œ ์•ˆ ์™ธ์šด ๋‹จ์–ด ์ค‘ ๋žœ๋ค 3๊ฐœ ํŒ์—…, ์ฒดํฌํ•œ ๋‹จ์–ด๋งŒ ์•”๊ธฐ ์™„๋ฃŒ ์ฒ˜๋ฆฌ | Once a day, on first visit, a popup shows 3 random unmemorized words; only checked ones are marked memorized | +| ๋ฌธ์„œ ์ฝ์Œ ์—ฌ๋ถ€ ํ† ๊ธ€, ์ˆ˜์ •/์‚ญ์ œ | Toggle read status, edit/delete articles | +| ๋ณธ์ธ์ด ๋“ฑ๋กํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ (์†Œ์œ ๊ถŒ ๊ฒ€์ฆ) | Only the owner can view their own data (ownership check) | + +--- + +## ๐Ÿ“ ๊ฐœ๋ฐœ ํ”„๋กœ์„ธ์Šค / Development Process + +**RPI(Research โ†’ Plan โ†’ Implement) Claude code ์›Œํฌํ”Œ๋กœ**๋ฅผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•ด์„œ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
+I customized the **RPI (Research โ†’ Plan โ†’ Implement) Calude code workflow** for this project. + +- **Research / Plan**: Claude Code๋กœ ์š”๊ตฌ์‚ฌํ•ญ์„ ๋ถ„์„ํ•˜๊ณ  `pm.md`(์š”๊ตฌ์‚ฌํ•ญ) / `ux.md`(ํ™”๋ฉด ํ๋ฆ„) / `eng.md`(๊ธฐ์ˆ  ์„ค๊ณ„) / `PLAN.md`(Phase๋ณ„ ์‹คํ–‰ ๊ณ„ํš)๋กœ ๋ฌธ์„œํ™”
+ Analyzed requirements with Claude Code and documented them as `pm.md` (requirements), `ux.md` (UX flows), `eng.md` (technical design), and `PLAN.md` (phase-by-phase execution plan). +- **Implement**: ๋ชจ๋“  ์ฝ”๋“œ๋Š” ๋ฏธ์…˜ ๋‹จ์œ„๋กœ ์ง์ ‘ ์ž‘์„ฑํ•˜๋ฉฐ ํ•™์Šต
+ Every line of code was written by hand, mission by mission, as a learning exercise. + +โ†’ ์„ค๊ณ„ ๋ฌธ์„œ ์ „์ฒด / Full design docs: [`rpi/article-vocab-tracker/`](./rpi/article-vocab-tracker/) + +์ด๋ ‡๊ฒŒ ๋‚˜๋ˆˆ ์ด์œ ๋Š”, ํ•™์Šต ๋‹จ๊ณ„์ด๊ธฐ ๋•Œ๋ฌธ์— ํšจ๊ณผ๋ฅผ ๊ทน๋Œ€ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์ง์ ‘ ์ž‘์„ฑํ•ด๋ณด๋ฉฐ ์ต์ˆ™ํ•ด์ง€๊ธฐ ์œ„ํ•ด์„œ์˜€์Šต๋‹ˆ๋‹ค.
+์„ค๊ณ„(Research/Plan)๋Š” AI์™€ ํ•จ๊ป˜ํ•˜๋˜, ์‹ค์ œ ๊ตฌํ˜„์€ ๊ฐœ๋…์„ ํ•˜๋‚˜์”ฉ ๋ฏธ์…˜์œผ๋กœ ๋ฐ›์•„ ์ง์ ‘ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.
+ +The reason for this split: since this was a learning project, writing every line myself and getting hands-on with each concept was the best way to actually absorb it. So design (Research/Plan) was done with AI, while implementation was done by receiving concepts as missions and writing every line by hand. + +--- + +## ๐Ÿ“ ๊ฐœ๋ฐœ ํžˆ์Šคํ† ๋ฆฌ (Phase๋ณ„) / Development History (by Phase) + +๊ฐ Phase๋Š” ๋ณ„๋„ ๋ธŒ๋žœ์น˜ + PR๋กœ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.
+Each phase was developed on its own branch and merged via PR. + +| Phase | ๋‚ด์šฉ / What | ํ•ต์‹ฌ ํ•™์Šต ๊ฐœ๋… / Key Concepts | +|---|---|---| +| 0 | ์Šค์บํด๋”ฉ + CI ์„ธํŒ… / Scaffolding + CI setup | `@Controller`, Thymeleaf, GitHub Actions (Checkstyle/SpotBugs) | +| 1 | ๋„๋ฉ”์ธ ๋ชจ๋ธ + ๋ฉ”๋ชจ๋ฆฌ Repository / Domain model + in-memory repository | Interface design, `Optional`, plain JUnit vs `@SpringBootTest` | +| 2 | ์•„ํ‹ฐํด CRUD / Article CRUD | `@PostMapping`, POST-Redirect-GET, domain methods for state changes | +| 3 | ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ / Category filter | Enum `@RequestParam`, Stream API, service-layer responsibility | +| 4 | ์ธ์ฆ + ์†Œ์œ ๊ถŒ / Auth + ownership | `HttpSession`, BCrypt, `HandlerInterceptor`, DTOs, ownership checks | +| 5 | ๋‹จ์–ด(Vocabulary) ๊ด€๋ฆฌ / Vocabulary management | FK-based 1:N relations, 1:N query methods | +| 6 | ๋‹จ์–ด ์•”๊ธฐ ํŒ์—… / Vocabulary memorization popup | Combining multiple repositories, random sampling, date-based state | +| 7 | ์Šคํƒ€์ผ๋ง + ์ˆ˜์ •/์‚ญ์ œ / Styling + edit/delete | Custom CSS design system, inline JS, completing Article CRUD | +| 8 | JPA + MySQL ์ „ํ™˜ / JPA + MySQL migration | `@Entity`, Spring Data JPA, `@Profile` swapping, `@Transactional` | + +**Phase 8์˜ ํ•ต์‹ฌ ์„ฑ๊ณผ / Key outcome of Phase 8**: Phase 1์—์„œ ์„ค๊ณ„ํ•œ Repository ์ธํ„ฐํŽ˜์ด์Šค ๋•๋ถ„์—, ๋ฉ”๋ชจ๋ฆฌ ๊ตฌํ˜„์ฒด โ†’ JPA/MySQL ๊ตฌํ˜„์ฒด๋กœ ์ „ํ™˜ํ•˜๋ฉด์„œ **Controller/Service ์ฝ”๋“œ๋ฅผ ํ•œ ์ค„๋„ ๋ฐ”๊พธ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.**
+Thanks to the repository interfaces designed in Phase 1, switching from the in-memory implementation to JPA/MySQL required **zero changes to Controller/Service code.** + +--- + +## ๐Ÿ› ๊ฒช์€ ์ด์Šˆ์™€ ํ•ด๊ฒฐ (์ผ๋ถ€) / Issues & Fixes (selected) + +- **MySQL ์˜ˆ์•ฝ์–ด ์ถฉ๋Œ / MySQL reserved word conflict**: `read`๊ฐ€ ์˜ˆ์•ฝ์–ด๋ผ ์ปฌ๋Ÿผ ์ƒ์„ฑ ์‹คํŒจ โ†’ `@Column(name = "is_read")`๋กœ ๋งคํ•‘ ๋ถ„๋ฆฌ
+ `read` is a reserved word, so table creation failed โ†’ mapped it explicitly with `@Column(name = "is_read")`. +- **JPA dirty checking ๋ฏธ๋ฐ˜์˜ / JPA dirty checking not applying**: ๋ฉ”๋ชจ๋ฆฌ ๊ตฌํ˜„์ฒด๋Š” ์ฐธ์กฐ ๊ณต์œ ๋กœ ํ•„๋“œ ๋ณ€๊ฒฝ์ด ์ฆ‰์‹œ ๋ฐ˜์˜๋์ง€๋งŒ, JPA๋Š” ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๋ฐ–์—์„œ๋Š” ๋ณ€๊ฒฝ์ด ๊ฐ์ง€๋˜์ง€ ์•Š์Œ โ†’ ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฉ”์„œ๋“œ์— `@Transactional` ์ถ”๊ฐ€
+ The in-memory repository shared object references, so field changes applied instantly; JPA only detects changes inside a transaction โ†’ added `@Transactional` to all state-mutating methods. +- **์„ธ์…˜ ๊ฐ์ฒด์™€ DB ์žฌ์กฐํšŒ ๊ฐ์ฒด๊ฐ€ ๋ณ„๊ฐœ ์ธ์Šคํ„ด์Šค์ธ ๋ฌธ์ œ / Session object vs. re-fetched DB object being separate instances**: ๋กœ๊ทธ์ธ ํŒ์—… ๋‚ ์งœ ๊ฐฑ์‹  ์‹œ DB ๋ฐ˜์˜๊ณผ ์„ธ์…˜ ๊ฐ์ฒด ๊ฐฑ์‹ ์„ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ˆ˜์ •
+ Fixed the login-popup date update to sync both the DB and the in-session object. +- **RDS ๋ณด์•ˆ ๊ทธ๋ฃน ๋ฏธ์„ค์ •์œผ๋กœ EC2 ์—ฐ๊ฒฐ ์‹คํŒจ / EC2 couldn't reach RDS due to missing security group rule**: RDS ์ธ๋ฐ”์šด๋“œ ๊ทœ์น™์— EC2 ๋ณด์•ˆ ๊ทธ๋ฃน์„ ์ถ”๊ฐ€ํ•ด ํ•ด๊ฒฐ
+ Resolved by adding the EC2 security group to the RDS inbound rules. + +--- + +## ๐Ÿš€ ๋ฐฐํฌ ์•„ํ‚คํ…์ฒ˜ / Deployment Architecture + +``` +์‚ฌ์šฉ์ž / User โ†’ devqueue.duckdns.org + โ†“ + EC2 (Amazon Linux 2023, Java 17, Spring Boot) + โ†“ (VPC ๋‚ด๋ถ€ ํ†ต์‹  / internal VPC traffic) + RDS (MySQL 8, Single-AZ) +``` + +- EC2์—์„œ `nohup`์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์‹คํ–‰ํ•ด SSH ์„ธ์…˜ ์ข…๋ฃŒ ํ›„์—๋„ ์œ ์ง€
+ Runs the app in the background with `nohup` on EC2, so it stays up after the SSH session ends. +- DuckDNS๋กœ ๋ฌด๋ฃŒ ์„œ๋ธŒ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ, 80๋ฒˆ ํฌํŠธ๋กœ ํฌํŠธ ๋ฒˆํ˜ธ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅ
+ Connected a free subdomain via DuckDNS, serving on port 80 so the URL needs no port number. + +--- + +## ๐Ÿ”ฎ ๋‹ค์Œ ๊ณ„ํš (v2 ์ดํ›„) / Roadmap (v2+) + +- ๊ฐœ๋ฐœ์ž ์œ ํ˜•(ํ”„๋ก ํŠธ/๋ฐฑ์—”๋“œ/๊ธฐํƒ€)๋ณ„ ์ธ๊ธฐ ๋งํฌ ๋žญํ‚น / Popular-link rankings by developer role (Frontend/Backend/Etc) +- Gmail OAuth2 ์†Œ์…œ ๋กœ๊ทธ์ธ ์ถ”๊ฐ€ / Gmail OAuth2 social login +- Category ๋™์  ๊ด€๋ฆฌ (ํ˜„์žฌ๋Š” Enum ๊ณ ์ •) / Dynamic category management (currently a fixed Enum) +- RDS ํผ๋ธ”๋ฆญ ์•ก์„ธ์Šค ๋น„ํ™œ์„ฑํ™” + Bastion Host ํŒจํ„ด ์ ์šฉ / Disable RDS public access + adopt a Bastion Host pattern (security hardening) +- ๋„๋ฉ”์ธ ๋™์  IP ๋Œ€์‘, HTTPS(SSL ์ธ์ฆ์„œ) ์ ์šฉ / Handle dynamic IP for the domain, add HTTPS (SSL certificate) + +--- + +## ๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ / Project Structure + +``` +dev-queue/ +โ”œโ”€โ”€ rpi/article-vocab-tracker/ # RPI ์›Œํฌํ”Œ๋กœ ์„ค๊ณ„ ๋ฌธ์„œ / RPI workflow design docs +โ”œโ”€โ”€ src/main/java/com/areumz/devqueue/ +โ”‚ โ”œโ”€โ”€ domain/ # Article, User, Vocabulary, Category, Role +โ”‚ โ”œโ”€โ”€ repository/ # ์ธํ„ฐํŽ˜์ด์Šค + memory/jpa ๊ตฌํ˜„์ฒด / interfaces + memory/jpa implementations +โ”‚ โ”œโ”€โ”€ service/ +โ”‚ โ”œโ”€โ”€ controller/ +โ”‚ โ””โ”€โ”€ config/ # SecurityConfig, WebConfig, LoginCheckInterceptor +โ”œโ”€โ”€ src/main/resources/ +โ”‚ โ”œโ”€โ”€ templates/ # Thymeleaf views +โ”‚ โ””โ”€โ”€ static/css/ # custom styles +โ””โ”€โ”€ .github/workflows/ci.yml # Checkstyle + SpotBugs +``` + +--- + +## ๐Ÿƒ ๋กœ์ปฌ ์‹คํ–‰ ๋ฐฉ๋ฒ• / Running Locally + +```bash +git clone https://github.com/areumz/dev-queue.git +cd dev-queue + +# application-local.properties.example ์ฐธ๊ณ ํ•˜์—ฌ +# application-local.properties ์ƒ์„ฑ (DB ์ •๋ณด ์ž…๋ ฅ) +# Copy application-local.properties.example to +# application-local.properties and fill in your DB credentials + +./gradlew bootRun +``` \ No newline at end of file diff --git a/docs/images/form.png b/docs/images/form.png new file mode 100644 index 0000000..f1deb5b Binary files /dev/null and b/docs/images/form.png differ diff --git a/docs/images/list.png b/docs/images/list.png new file mode 100644 index 0000000..24f4d75 Binary files /dev/null and b/docs/images/list.png differ diff --git a/docs/images/signin.png b/docs/images/signin.png new file mode 100644 index 0000000..ab146d3 Binary files /dev/null and b/docs/images/signin.png differ diff --git a/docs/images/word-popup.png b/docs/images/word-popup.png new file mode 100644 index 0000000..dd9a4a5 Binary files /dev/null and b/docs/images/word-popup.png differ diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index d3e9a18..0000000 Binary files a/src/.DS_Store and /dev/null differ