From 28a7a91ca22013355e39fdddfe45e9ab2834d8f4 Mon Sep 17 00:00:00 2001 From: sunmingLee <25thbam@gmail.com> Date: Fri, 24 Oct 2025 11:04:59 +0900 Subject: [PATCH 01/61] =?UTF-8?q?Swagger=20=EA=B4=80=EB=A0=A8=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=82=AC=ED=95=AD=20=EB=A1=A4=EB=B0=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gradle/8.10/checksums/checksums.lock | Bin 17 -> 17 bytes .gradle/8.10/checksums/md5-checksums.bin | Bin 73965 -> 121833 bytes .gradle/8.10/checksums/sha1-checksums.bin | Bin 153107 -> 268979 bytes .../executionHistory/executionHistory.bin | Bin 85985 -> 275816 bytes .../executionHistory/executionHistory.lock | Bin 17 -> 17 bytes .gradle/8.10/fileHashes/fileHashes.bin | Bin 20297 -> 25397 bytes .gradle/8.10/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .../8.10/fileHashes/resourceHashesCache.bin | Bin 19075 -> 21217 bytes .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .gradle/buildOutputCleanup/outputFiles.bin | Bin 18965 -> 19847 bytes .gradle/file-system.probe | Bin 8 -> 8 bytes .run/DistributionServiceApplication.run.xml | 20 ++ claude/make-run-profile.md | 175 ++++++++++ .../.run/distribution-service.run.xml | 51 +++ .../distribution/DistributionApplication.java | 23 ++ .../adapter/AbstractChannelAdapter.java | 86 +++++ .../distribution/adapter/ChannelAdapter.java | 30 ++ .../distribution/adapter/GiniTvAdapter.java | 45 +++ .../adapter/InstagramAdapter.java | 45 +++ .../kt/distribution/adapter/KakaoAdapter.java | 45 +++ .../kt/distribution/adapter/NaverAdapter.java | 45 +++ .../distribution/adapter/RingoBizAdapter.java | 45 +++ .../adapter/UriDongNeTvAdapter.java | 72 +++++ .../kt/distribution/config/KafkaConfig.java | 46 +++ .../com/kt/distribution/config/WebConfig.java | 32 ++ .../controller/DistributionController.java | 71 ++++ .../dto/ChannelDistributionResult.java | 49 +++ .../kt/distribution/dto/ChannelStatus.java | 100 ++++++ .../com/kt/distribution/dto/ChannelType.java | 26 ++ .../distribution/dto/DistributionRequest.java | 54 ++++ .../dto/DistributionResponse.java | 63 ++++ .../dto/DistributionStatusResponse.java | 52 +++ .../event/DistributionCompletedEvent.java | 48 +++ .../DistributionStatusRepository.java | 65 ++++ .../service/DistributionService.java | 261 +++++++++++++++ .../service/KafkaEventPublisher.java | 62 ++++ .../src/main/resources/application.yml | 102 ++++++ .../src/main/resources/mock-events.json | 134 ++++++++ distribution-service/test-distribution.sh | 34 ++ tools/run-intellij-service-profile.py | 303 ++++++++++++++++++ 40 files changed, 2184 insertions(+) create mode 100644 .run/DistributionServiceApplication.run.xml create mode 100644 claude/make-run-profile.md create mode 100644 distribution-service/.run/distribution-service.run.xml create mode 100644 distribution-service/src/main/java/com/kt/distribution/DistributionApplication.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/AbstractChannelAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/ChannelAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/GiniTvAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/InstagramAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/KakaoAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/NaverAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/RingoBizAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/adapter/UriDongNeTvAdapter.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/config/KafkaConfig.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/config/WebConfig.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/controller/DistributionController.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/ChannelDistributionResult.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/ChannelStatus.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/ChannelType.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/DistributionRequest.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/DistributionResponse.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/dto/DistributionStatusResponse.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/event/DistributionCompletedEvent.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/repository/DistributionStatusRepository.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/service/DistributionService.java create mode 100644 distribution-service/src/main/java/com/kt/distribution/service/KafkaEventPublisher.java create mode 100644 distribution-service/src/main/resources/application.yml create mode 100644 distribution-service/src/main/resources/mock-events.json create mode 100644 distribution-service/test-distribution.sh create mode 100644 tools/run-intellij-service-profile.py diff --git a/.gradle/8.10/checksums/checksums.lock b/.gradle/8.10/checksums/checksums.lock index 837e5b9337bcfdbcf1aebd76ad9c0a8bfa0490ec..a1da254f90f45a01d64a9a5037595482128dd811 100644 GIT binary patch literal 17 VcmZQJJh3L%MKEUr0~qj!0RSw-1MC0* literal 17 VcmZQJJh3L%MKEUr0~l~D0RSxo1Tz2t diff --git a/.gradle/8.10/checksums/md5-checksums.bin b/.gradle/8.10/checksums/md5-checksums.bin index 04c6d0050987548d4ed9b0f3828b171e49d75fb8..589eb506a531946591ec1f98073f16c15ff73b30 100644 GIT binary patch delta 20671 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 LeJ7&W#^-+nrhwWc diff --git a/.gradle/8.10/checksums/sha1-checksums.bin b/.gradle/8.10/checksums/sha1-checksums.bin index 19a54106b689d3aaf682f717492529d1d26814ea..73ed738ef00ba4f482d387c458d98d3d467e884e 100644 GIT binary patch delta 49507 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 p{qr0q diff --git a/.gradle/8.10/executionHistory/executionHistory.bin b/.gradle/8.10/executionHistory/executionHistory.bin index 2177cdd01b3d65d3f655cadb7b28c6362b23ce72..3778430bf1cffdf4a829a73cbfc54ca5b7c22791 100644 GIT binary patch literal 275816 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

0U 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%0ATASsf}RDECj?>vR7CaH`1e*IEdZqrMN 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 delta 2378 zcmeHHX;4#F6z0AM0a7TBJeHtPQ0r*5LMVeR$dph(5=AsBOO>GnlpU&q5f}wU3vFqK z;E`Np7o>=_vc#8j2q=g`DWdj$RI8J`co@c9NWCI?}-X&5!a>x5;r z)Im$j+>-nznm<2jeP~Q_TtxJiu(0rTkx`MMj`5qLV#v3iG07~a$Ct8D)g-O}Zu^TyBQ?COuQAS#E{Kr@RJ9>G4kL)Up)V|cTVr3XOUw#PZzo` zR58y!$Z|^H#`s5@S?3*$`=TC7eye5%EcR}?`?vcMEB$e6Th7hJI9p7aL4(8;J)BY< zT*-ljE3MF37-))xMU56FG?PeoZk48XjW;C=k4<~tcpX*2##N@8AbRw2B9^;R`uMDn zvO$exstgy(C>}pbDwER8e(#~WCl4Oo)gNQ5otx|&LvlOczb`r9|1kOb%5nSbY&4+xvhusVpI0o#4GL3QZ| z9;YUyF7Zl1mrL^Z8L?T8dZ<>TuSlX89Zdm)FKdL906lo*XHBUnSaBhmxCVGoh%9(C zX}>Vs8U~D<~JuTh$LxbDnCvCl^uSsVR+tPhvof~0^FTQ`emy+z>H@Y#|ho) zYTNUiZnPXNQQ#F{AbZq5naBc;d^sIGpY*0-Yjg@uPDN3C;+i~MA>LxeP5nj9ao zQOY<)O?%(IxuE00Y=O;1+oH%8&RY<+kDxq{*N0P09LjYBy352^ZH}~I8#aYGIjeE= zdYVNqUc(gzNDuzX@rAY9S=0am90CoBAY2$mvtWnM!|5&-HWn`g=iERD?q*RZoXKyY$|lPhnz_HeM%dvjY-`%^TTnVjAp4zG}sp|f#QNxEKEWyx;-^xQp^{hN+fH0 zHif^X#Nw_tqyv(ol{hnrQ2VB+Q}q}Yufi*~BF%z#W7j>T=r?+}rDQd>-9ZSY>CO=R zvk`9CP9#Zp@rK`NZE)Ww!4Fc2cK@qAFah1trC`*@l84~69bw8i&|JD2XXTSiWnWf= z2X_(at}>;fkN|=m5*Xg^g$=(Uf|{m=Ky|qvoPNlnyAwJBOVXjNYD3uclQ&LDC#5o9!Qp&9uR;2B7s53>Cc!M)F z7cyFTbn~zR3R=b7OA|4bF)GIU79&1B>_TzCu#HEP0JiZ$AJZzrl(f0hrwEYkh33=s z1US>i$Je}&fG!zPV6F)H&~?OED?)R*PbL&|7okEdNTe)rr3kSU37!_A1#}rP9*WQ^ wI*S+{Vr0iH9?=wc$Pw+qEmE4raxvo3-;rp!7 diff --git a/.gradle/8.10/executionHistory/executionHistory.lock b/.gradle/8.10/executionHistory/executionHistory.lock index 0ce4c9646ca85d214092028f7db63bee6e79e803..778ae703f930bcf78ae246bed6ada1f3858ddff5 100644 GIT binary patch literal 17 UcmZRsbes@(Px9DK1_RL07c2uXt^ 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 diff --git a/.gradle/8.10/fileHashes/fileHashes.lock b/.gradle/8.10/fileHashes/fileHashes.lock index 340e0dd0673653407cd5d6c667877cdda9e87606..4577896007bea6b094d682bc2b7981c78375319a 100644 GIT binary patch literal 17 VcmZS9`MA7`o8<*R0~j#c0st$`1C{^) literal 17 UcmZS9`MA7`o8<*R0|dAO04vo4p8x;= diff --git a/.gradle/8.10/fileHashes/resourceHashesCache.bin b/.gradle/8.10/fileHashes/resourceHashesCache.bin index 3d2189638c23437e4cea73219677f8823a00620a..4320628bdf822d3491500c210841da4ae2445301 100644 GIT binary patch literal 21217 zcmeI(do)yQ9|v$H22mQ7TS_ibLgkV|jml*(GiH#GOE+o|!%411j8HD69Ji=OqTJ^6 zMlT_^h}_WxA^& zNM$AB)F|L7iSRUp>^hP+4F=xd=N?f*efbofuLt~Nv15-_vt}vcR2SgCzmx0` zjW56>P80(E#p1PtbA^{M;^ecyf4q*8+0viHM4aXid~sk)cSq%F3)HUx=N8x4+Jh-UzH~!&ufmj@dw}v z?K351ZZ91WH+>I$^M|A5qkZMe(fJhM%Bxp|M^*F_5vPs;*J5gv#{QYfi+T%i?WwoL zw+b$HAx^vsTqiE&!Ch|=bU#xz0@n-EB;)C}=;vzyVqAWq^3ZhcqKni^k|gwB5k+~MF4U5$^70>q7_<~)!;A>B&e3H2J_$1aW$ zq|;dFx@2+SPT?()icuozX#NmzI=4ZBxMk-h#A()ZUSX1%%tJ))pGLQVGmgmJwO8EP zgyc;az&#BOE-D0k+lDxK0QhN>GsemB=g@sl+Ya2&7Mv zojFpViA}r;$A?8clDHM$6<%FGqvelHOtn%+!iFV9PwqXalBrk3#wKuenY{X`Lw>G1 z1=Okq;i3_tJOobZ9!`v8xfVIym`*>}<- zQ2(q;q~XeQoC@ZhZCc!g8^0Lsa=^*6?(D8V{sfyatq$_K8nEqt&a2|{6<1QJ*aQ!G z%k&k+{1vt2+~z?!_AoY~YDH*T#O!rf#c8cxaWc~nn;`Uy`0SHWXPNZ27~ST{)x{>R zdMOqA*)LKEtCha3@|Y;OK;p@-=h}Vkqh9yf7>Bb`7ns3z#$T<^Hqd-1xHnmB=c`cc zj3UOSH%}D#m9*5OH8XqN6R`=ngZ_EF{Co%fy!C%$2mSn$`!{xwQgIkRY((EQ%-__cmhU)2E4(?#!Ewa*+z1{9Z1dDr`bK?2p#Ux~17s9jzI;pH;`P ziKZ9(#h+z7g`Y{F84*>PFLSDm^3z@2E(ns?X)>518(T25zu?K(z~ zd#-gVHlfo^5h<%?9pWWfOWdilYr`g^Synfd&3H#B>CeXz{Pi zn@;lDbpQD{3A;+j0NJn3N|6Lgh+OL zWS^GBk30SNhq(tfA;dG18=d{)^;lL5P5RGuo3IIq1djuj%^JgbIxI%ry8_O$H}kHB zBCoYla&1z*e06lAt~P%yY+}tepP|mCE29cpK7AYF?!Un%JQWolCRVwms;#EdaysQ4 zv58L?weQU=df{3WmNn7!mc@BSWZv%)rDg59EXQ%8=^e7WwXr zZ=Q0nk8)lWxbZmWDWG|)1cyg=sN4G-&R1F@l`-yTg-v8Cj5H~cKYV_z9sBLbL%s#} zhnsnHWhm48H|F!b^b$EYf=cCkoeKbyS!^C*vud{ zyiX!%f&CFvL`z%Wt5$XFBY&a$xOFUcMuvvAS^tPEPeprH>9sxOOR$ORwjiASkuSKc z*Ks9=L2uS$6IU%(?%$@e)VM*BkiUD6Tnskh|NUk?Y43Hvi@YiO$q_XR+@+K@Y`ZGw zW7SQFo8F;(*z*l`1~D=6LY561uPU}9RPEMiI5uJ7@= diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 0350ff23745792ae6b0e29c9fdba156ee56cca00..06bcff59a652a195edd84d8b91bec88354f003ce 100644 GIT binary patch literal 17 VcmZQ(PG7Ze^~s_h1~6cB001+%1c?9u literal 17 UcmZQ(PG7Ze^~s_h1_&?)05hosdjJ3c diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index 4ed6f06d6395816365de6075047efe060d9cdefb..695f9c2a60314a1331c210d89e786645e64cb8b8 100644 GIT binary patch literal 19847 zcmeI&YfMvT7{Kv5qop{CI*_YiWI72{VHMfJhy!OhJ*R~+ML=$01G-EWoTS5X*@lLp zf)~U|gbg9{0%Ontl_@t7TO8w(v4tTRVpB!{CkhVM5ihLdeV>|rBmF?w@|*-X`St00 z&ikkB7kG=uvt2n$58m^``uQoIVF4_F1+V}XzyeqR3t#~(fCaDs7Qg~n01IFNEPw^D z02aUk|Ca(GhK-n$&DgrXFn3~^!sB_oXgJ|A8`HZVhEA%j^!*f95)P%fP6nZPw#9T}NQ_ ziNG>dTE|v;-4Jkf^`L(1y9rUmC3fJNI!{-Xm*ZD--U7VS#ZK!K#?a18C3x2r-I1Xq ze`nL{I)MMOUoP`6Of4bK6oK~+ms|K>YT8KWw}by?Rd%e0<7yb?TJ)>9o9TeI81?#7PkDq^>FNhypM&`Ly;M30zuOGb-yqtOlxc=Kh8?9e; zK5?lW{8{oAPt|nbY3jq^Oa1vRY(&*A;%q)RKRBjAqTM|ARyG_GUvRVQZbf&SEDNbm zf(x9((#B`*D~WS1;4AEPt1HVBX;1bCaN(x5HJ2JS_GF#~p0&JbR%4OZ5lvkBD?DId zS~69vOMQd-C*Tf)UFUY!JUB!>2i$4@R99Wa`a5*}SHRbmG6A3Nev={2d4ap;S!71^ zDYSGx3S8u+O5fx4ofUDZr_p;{Iv?aFU7_*cKq_$y#5+G zUjV+RI(J{7yuFvsPlJc=T-(%`V?RQi8v~DUQxEJ7b(*2;1c65uGh*}TKj`NPdj~x3 zp#$4t=9xy<*$4iC(uVoQVI}P*i3LwoSJf9tr>Dp~n`!i}<1zQrl7s0wrQjcpHS}El zy?6z^A3pej4l`x*l-`-H(+>V|pCCJHKsip=2?tLvUnd<1lHVgP3kHXuJRaYUJci$~ z02aUkSO5!P0W5$8umBdo0$2bGU;!+E1+V}XzyeqR3t#~(fCaDs7Qg~n01IFNEPw^D z02aUkSO5!P0W7fi3y2N3IQI|jOXwF*_%E5`|GpSDVZtss9~& delta 113 zcmZpl%{X-m;|3E6L8+cNXOH||M^y$e@OV7=t%Sm4RY{4-XMtE$N@B7r5SIe+QmKl` zzS0tt?@Aj?7L_%a94jj^d9N%`Oip6*R3N@9C&BX{3K$re8aFEb5Z`DpLv*vF#}!5Z DJ_{*- diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe index ac4beb46220d110a11f9e5f196fa452a079e920d..2037b03cd40a019ddb04bbbb0d6ea81296790f97 100644 GIT binary patch literal 8 PcmZQzV4NlVWQHvO2Xz8y literal 8 PcmZQzV4Nl3y6qtV25bU| diff --git a/.run/DistributionServiceApplication.run.xml b/.run/DistributionServiceApplication.run.xml new file mode 100644 index 0000000..664df2a --- /dev/null +++ b/.run/DistributionServiceApplication.run.xml @@ -0,0 +1,20 @@ + + + + diff --git a/claude/make-run-profile.md b/claude/make-run-profile.md new file mode 100644 index 0000000..f363a91 --- /dev/null +++ b/claude/make-run-profile.md @@ -0,0 +1,175 @@ +# 서비스실행파ì¼ìž‘성가ì´ë“œ + +[요청사항] +- <수행ì›ì¹™>ì„ ì¤€ìš©í•˜ì—¬ 수행 +- <수행순서>ì— ë”°ë¼ ìˆ˜í–‰ +- [결과파ì¼] ì•ˆë‚´ì— ë”°ë¼ íŒŒì¼ ìž‘ì„± + +[ê°€ì´ë“œ] +<수행ì›ì¹™> +- 설정 Manifest(src/main/resources/application*.yml)ì˜ ê° í•­ëª©ì˜ ê°’ì€ í•˜ë“œì½”ë”©í•˜ì§€ 않고 환경변수 처리 +- Kubernetesì— ë°°í¬ëœ ë°ì´í„°ë² ì´ìŠ¤ëŠ” LoadBalacerìœ í˜•ì˜ Service를 만들어 ì—°ê²° +- MQ ì´ìš© 시 'MQ설치결과서'ì˜ ì—°ê²° 정보를 실행 프로파ì¼ì˜ 환경변수로 ë“±ë¡ +<수행순서> +- 준비: + - ë°ì´í„°ë² ì´ìŠ¤ì„¤ì¹˜ê²°ê³¼ì„œ(develop/database/exec/db-exec-dev.md) ë¶„ì„ + - ìºì‹œì„¤ì¹˜ê²°ê³¼ì„œ(develop/database/exec/cache-exec-dev.md) ë¶„ì„ + - MQ설치결과서(develop/mq/mq-exec-dev.md) ë¶„ì„ - ì—°ê²° ì •ë³´ í™•ì¸ + - kubectl get svc -n tripgen-dev | grep LoadBalancer 실행하여 External IP ëª©ë¡ í™•ì¸ +- 실행: + - ê° ì„œë¹„ìŠ¤ë³„ë¥¼ 서브ì—ì´ì ¼íŠ¸ë¡œ 병렬 수행 + - 설정 Manifest 수정 + - 하드코딩 ë˜ì–´ 있는 ê°’ì´ ìžˆìœ¼ë©´ 환경변수로 변환 + - 특히, ë°ì´í„°ë² ì´ìФ, MQ ë“±ì˜ ì—°ê²° 정보는 반드시 환경변수로 변환해야 함 + - 민ê°í•œ ì •ë³´ì˜ ë””í…íŠ¸ê°’ì€ ìƒëžµí•˜ê±°ë‚˜ 간략한 값으로 지정 + - '<로그설정>'ì„ ì°¸ì¡°í•˜ì—¬ Log íŒŒì¼ ì„¤ì • + - '<ì‹¤í–‰í”„ë¡œíŒŒì¼ ìž‘ì„± ê°€ì´ë“œ>'ì— ë”°ë¼ ì„œë¹„ìŠ¤ ì‹¤í–‰í”„ë¡œíŒŒì¼ ìž‘ì„± + - LoadBalancer External IP를 DB_HOST, REDIS_HOST로 설정 + - MQ ì—°ê²° 정보를 application.ymlì˜ í™˜ê²½ë³€ìˆ˜ëª…ì— ë§žì¶° 설정 + - 서비스 실행 ë° ì˜¤ë¥˜ 수정 + - 'IntelliJ서비스실행기'를 'tools' ë””ë ‰í† ë¦¬ì— ë‹¤ìš´ë¡œë“œ + - python ë˜ëŠ” python3 명령으로 백그ë¼ìš°ë“œë¡œ 실행하고 ê²°ê³¼ 로그를 ë¶„ì„ + nohup python3 tools/run-intellij-service-profile.py {service-name} > logs/{service-name}.log 2>&1 & echo "Started {service-name} with PID: $!" + - 서비스 ì‹¤í–‰ì€ ë‹¤ë¥¸ 방법 사용하지 ë§ê³  **반드시 python 프로그램 ì´ìš©** + - 오류 수정 후 í•„ìš” 시 실행파ì¼ì˜ 환경변수를 올바르게 변경 + - 서비스 ì •ìƒ ì‹œìž‘ í™•ì¸ í›„ 서비스 중지 + - ê²°ê³¼: {service-name}/.run +<서비스 중지 방법> +- Window + - netstat -ano | findstr :{PORT} + - powershell "Stop-Process -Id {Process number} -Force" +- Linux/Mac + - netstat -ano | grep {PORT} + - kill -9 {Process number} +<로그설정> +- **application.yml 로그 íŒŒì¼ ì„¤ì •**: + ```yaml + logging: + file: + name: ${LOG_FILE:logs/trip-service.log} + logback: + rollingpolicy: + max-file-size: 10MB + max-history: 7 + total-size-cap: 100MB + ``` + +<ì‹¤í–‰í”„ë¡œíŒŒì¼ ìž‘ì„± ê°€ì´ë“œ> +- {service-name}/.run/{service-name}.run.xml 파ì¼ë¡œ 작성 +- Spring Bootê°€ 아니고 **Gradle 실행 프로파ì¼**ì´ì–´ì•¼ 함: '[ì‹¤í–‰í”„ë¡œíŒŒì¼ ì˜ˆì‹œ]' 참조 +- Kubernetesì— ë°°í¬ëœ ë°ì´í„°ë² ì´ìŠ¤ì˜ LoadBalancer Service 확ì¸: + - kubectl get svc -n {namespace} | grep LoadBalancer 명령으로 LoadBalancer IP í™•ì¸ + - ê° ì„œë¹„ìŠ¤ë³„ ë°ì´í„°ë² ì´ìŠ¤ì˜ LoadBalancer External IP를 DB_HOST로 사용 + - ìºì‹œ(Redis)ì˜ LoadBalancer External IP를 REDIS_HOST로 사용 +- MQ ì—°ê²° 설정: + - MQ설치결과서(develop/mq/mq-exec-dev.md)ì—서 ì—°ê²° ì •ë³´ í™•ì¸ + - MQ ìœ í˜•ì— ë”°ë¥¸ ì—°ê²° ì •ë³´ 설정 예시: + - RabbitMQ: RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_USERNAME, RABBITMQ_PASSWORD + - Kafka: KAFKA_BOOTSTRAP_SERVERS, KAFKA_SECURITY_PROTOCOL + - Azure Service Bus: SERVICE_BUS_CONNECTION_STRING + - AWS SQS: AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY + - Redis (Pub/Sub): REDIS_HOST, REDIS_PORT, REDIS_PASSWORD + - ActiveMQ: ACTIVEMQ_BROKER_URL, ACTIVEMQ_USER, ACTIVEMQ_PASSWORD + - 기타 MQ: 해당 MQì˜ ì—°ê²°ì— í•„ìš”í•œ 호스트, í¬íЏ, ì¸ì¦ì •ë³´, 연결문ìžì—´ ë“±ì„ í™˜ê²½ë³€ìˆ˜ë¡œ 설정 + - application.ymlì— ì •ì˜ëœ 환경변수명 í™•ì¸ í›„ 매핑 +- 백킹서비스 ì—°ê²° ì •ë³´ 매핑: + - ë°ì´í„°ë² ì´ìŠ¤ì„¤ì¹˜ê²°ê³¼ì„œì—서 ê° ì„œë¹„ìŠ¤ë³„ DB ì¸ì¦ ì •ë³´ í™•ì¸ + - ìºì‹œì„¤ì¹˜ê²°ê³¼ì„œì—서 ê° ì„œë¹„ìŠ¤ë³„ Redis ì¸ì¦ ì •ë³´ í™•ì¸ + - LoadBalancerì˜ External IP를 호스트로 사용 (ë‚´ë¶€ DNS 아님) +- ê°œë°œëª¨ë“œì˜ DDL_AUTOê°’ì€ update로 함 +- JWT Secret Key는 모든 서비스가 ë™ì¼í•´ì•¼ 함 +- application.yamlì˜ í™˜ê²½ë³€ìˆ˜ì™€ ì¼ì¹˜í•˜ë„ë¡ í™˜ê²½ë³€ìˆ˜ 설정 +- application.yamlì˜ ë¯¼ê° ì •ë³´ëŠ” 기본값으로 지정하지 않고 실제 백킹서비스 정보로 지정 +- 백킹서비스 ì—°ê²° í™•ì¸ ê²°ê³¼ë¥¼ 바탕으로 정확한 ê°’ì„ ì§€ì • +- ê¸°ì¡´ì— íŒŒì¼ì´ 있으면 ë‚´ìš©ì„ ë¶„ì„하여 항목 추가/수정/ì‚­ì œ + +[ì‹¤í–‰í”„ë¡œíŒŒì¼ ì˜ˆì‹œ] +``` + + + + + + + + true + true + + + + + false + false + + + +``` + +[참고ìžë£Œ] +- ë°ì´í„°ë² ì´ìŠ¤ì„¤ì¹˜ê²°ê³¼ì„œ: develop/database/exec/db-exec-dev.md + - ê° ì„œë¹„ìŠ¤ë³„ DB ì—°ê²° ì •ë³´ (사용ìžëª…, 비밀번호, DB명) + - LoadBalancer Service External IP ëª©ë¡ +- ìºì‹œì„¤ì¹˜ê²°ê³¼ì„œ: develop/database/exec/cache-exec-dev.md + - ê° ì„œë¹„ìŠ¤ë³„ Redis ì—°ê²° ì •ë³´ + - LoadBalancer Service External IP ëª©ë¡ +- MQ설치결과서: develop/mq/mq-exec-dev.md + - MQ 유형 ë° ì—°ê²° ì •ë³´ + - ì—°ê²°ì— í•„ìš”í•œ 호스트, í¬íЏ, ì¸ì¦ ì •ë³´ + - LoadBalancer Service External IP (해당하는 경우) diff --git a/distribution-service/.run/distribution-service.run.xml b/distribution-service/.run/distribution-service.run.xml new file mode 100644 index 0000000..b3879c4 --- /dev/null +++ b/distribution-service/.run/distribution-service.run.xml @@ -0,0 +1,51 @@ + + + + + + + + true + true + + + + + false + false + + + diff --git a/distribution-service/src/main/java/com/kt/distribution/DistributionApplication.java b/distribution-service/src/main/java/com/kt/distribution/DistributionApplication.java new file mode 100644 index 0000000..2534d29 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/DistributionApplication.java @@ -0,0 +1,23 @@ +package com.kt.distribution; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.kafka.annotation.EnableKafka; + +/** + * Distribution Service Application + * 다중 ì±„ë„ ë°°í¬ ê´€ë¦¬ 서비스 + * + * @author System Architect + * @since 2025-10-23 + */ +@SpringBootApplication +@EnableKafka +@EnableFeignClients +public class DistributionApplication { + + public static void main(String[] args) { + SpringApplication.run(DistributionApplication.class, args); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/AbstractChannelAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/AbstractChannelAdapter.java new file mode 100644 index 0000000..c0bebce --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/AbstractChannelAdapter.java @@ -0,0 +1,86 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.DistributionRequest; +import io.github.resilience4j.bulkhead.annotation.Bulkhead; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; +import lombok.extern.slf4j.Slf4j; + +/** + * Abstract Channel Adapter + * 공통 ë¡œì§ ë° Resilience4j ì ìš© + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +public abstract class AbstractChannelAdapter implements ChannelAdapter { + + /** + * 채ë„로 ë°°í¬ ì‹¤í–‰ (Resilience4j ì ìš©) + * + * @param request DistributionRequest + * @return ChannelDistributionResult + */ + @Override + @CircuitBreaker(name = "channelApi", fallbackMethod = "fallback") + @Retry(name = "channelApi") + @Bulkhead(name = "channelApi") + public ChannelDistributionResult distribute(DistributionRequest request) { + long startTime = System.currentTimeMillis(); + + try { + log.info("Starting distribution to channel: {}, eventId: {}", + getChannelType(), request.getEventId()); + + // 실제 외부 API 호출 (구현체ì—서 구현) + ChannelDistributionResult result = executeDistribution(request); + result.setExecutionTimeMs(System.currentTimeMillis() - startTime); + + log.info("Distribution completed successfully: channel={}, eventId={}, executionTime={}ms", + getChannelType(), request.getEventId(), result.getExecutionTimeMs()); + + return result; + + } catch (Exception e) { + long executionTime = System.currentTimeMillis() - startTime; + log.error("Distribution failed: channel={}, eventId={}, error={}", + getChannelType(), request.getEventId(), e.getMessage(), e); + + return ChannelDistributionResult.builder() + .channel(getChannelType()) + .success(false) + .errorMessage(e.getMessage()) + .executionTimeMs(executionTime) + .build(); + } + } + + /** + * 실제 외부 API 호출 ë¡œì§ (구현체ì—서 구현) + * + * @param request DistributionRequest + * @return ChannelDistributionResult + */ + protected abstract ChannelDistributionResult executeDistribution(DistributionRequest request); + + /** + * Fallback 메서드 (Circuit Breaker Open 시) + * + * @param request DistributionRequest + * @param throwable Throwable + * @return ChannelDistributionResult + */ + protected ChannelDistributionResult fallback(DistributionRequest request, Throwable throwable) { + log.warn("Fallback triggered for channel: {}, eventId: {}, reason: {}", + getChannelType(), request.getEventId(), throwable.getMessage()); + + return ChannelDistributionResult.builder() + .channel(getChannelType()) + .success(false) + .errorMessage("Circuit Breaker Open: " + throwable.getMessage()) + .executionTimeMs(0) + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/ChannelAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/ChannelAdapter.java new file mode 100644 index 0000000..bfedfc7 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/ChannelAdapter.java @@ -0,0 +1,30 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; + +/** + * Channel Adapter Interface + * ê° ì±„ë„별 ë°°í¬ API를 호출하는 ì¸í„°íŽ˜ì´ìФ + * + * @author System Architect + * @since 2025-10-23 + */ +public interface ChannelAdapter { + + /** + * ì§€ì›í•˜ëŠ” ì±„ë„ íƒ€ìž… + * + * @return ChannelType + */ + ChannelType getChannelType(); + + /** + * 채ë„로 ë°°í¬ ì‹¤í–‰ + * + * @param request DistributionRequest + * @return ChannelDistributionResult + */ + ChannelDistributionResult distribute(DistributionRequest request); +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/GiniTvAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/GiniTvAdapter.java new file mode 100644 index 0000000..655d9a6 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/GiniTvAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * 지니TV Adapter + * 지니TV ê´‘ê³  ë“±ë¡ API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class GiniTvAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.ginitv.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.GINITV; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling GiniTV API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "GTIV-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.GINITV) + .success(true) + .distributionId(distributionId) + .estimatedReach(10000) // TV ê´‘ê³  노출 수 + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/InstagramAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/InstagramAdapter.java new file mode 100644 index 0000000..3b98443 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/InstagramAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * Instagram Adapter + * Instagram í¬ìŠ¤íŒ… API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class InstagramAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.instagram.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.INSTAGRAM; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling Instagram API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "INSTA-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.INSTAGRAM) + .success(true) + .distributionId(distributionId) + .estimatedReach(3000) // 팔로워 수 기반 ì˜ˆìƒ ë…¸ì¶œ + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/KakaoAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/KakaoAdapter.java new file mode 100644 index 0000000..68c7e06 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/KakaoAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * Kakao Channel Adapter + * Kakao Channel í¬ìŠ¤íŒ… API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class KakaoAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.kakao.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.KAKAO; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling Kakao API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "KAKAO-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.KAKAO) + .success(true) + .distributionId(distributionId) + .estimatedReach(4000) // ì±„ë„ ì¹œêµ¬ 수 기반 + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/NaverAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/NaverAdapter.java new file mode 100644 index 0000000..0d7f44e --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/NaverAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * Naver Blog Adapter + * Naver Blog í¬ìŠ¤íŒ… API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class NaverAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.naver.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.NAVER; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling Naver API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "NAVER-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.NAVER) + .success(true) + .distributionId(distributionId) + .estimatedReach(2000) // 블로그 ë°©ë¬¸ìž ìˆ˜ 기반 + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/RingoBizAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/RingoBizAdapter.java new file mode 100644 index 0000000..8ec0634 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/RingoBizAdapter.java @@ -0,0 +1,45 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * ë§ê³ ë¹„즈 Adapter + * ë§ê³ ë¹„즈 ì—°ê²°ìŒ ì—…ë°ì´íЏ API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class RingoBizAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.ringobiz.url}") + private String apiUrl; + + @Override + public ChannelType getChannelType() { + return ChannelType.RINGOBIZ; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling RingoBiz API: url={}, eventId={}", apiUrl, request.getEventId()); + + // TODO: 실제 API 호출 (현재는 Mock) + String distributionId = "RBIZ-" + UUID.randomUUID().toString(); + + return ChannelDistributionResult.builder() + .channel(ChannelType.RINGOBIZ) + .success(true) + .distributionId(distributionId) + .estimatedReach(1000) // ì—°ê²°ìŒ ì‚¬ìš©ìž ìˆ˜ + .build(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/adapter/UriDongNeTvAdapter.java b/distribution-service/src/main/java/com/kt/distribution/adapter/UriDongNeTvAdapter.java new file mode 100644 index 0000000..41fa264 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/adapter/UriDongNeTvAdapter.java @@ -0,0 +1,72 @@ +package com.kt.distribution.adapter; + +import com.kt.distribution.dto.ChannelDistributionResult; +import com.kt.distribution.dto.ChannelType; +import com.kt.distribution.dto.DistributionRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 우리ë™ë„¤TV Adapter + * 우리ë™ë„¤TV API 호출 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Component +public class UriDongNeTvAdapter extends AbstractChannelAdapter { + + @Value("${channel.apis.uridongnetv.url}") + private String apiUrl; + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + public ChannelType getChannelType() { + return ChannelType.URIDONGNETV; + } + + @Override + protected ChannelDistributionResult executeDistribution(DistributionRequest request) { + log.debug("Calling UriDongNeTV API: url={}, eventId={}", apiUrl, request.getEventId()); + + // 외부 API 호출 준비 + Map payload = new HashMap<>(); + payload.put("eventId", request.getEventId()); + payload.put("title", request.getTitle()); + payload.put("videoUrl", request.getImageUrl()); // ì´ë¯¸ì§€ë¥¼ ì˜ìƒìœ¼ë¡œ 변환한 URL + payload.put("radius", getChannelSetting(request, "radius", "500m")); + payload.put("timeSlot", getChannelSetting(request, "timeSlot", "evening")); + + // TODO: 실제 API 호출 (현재는 Mock) + // ResponseEntity response = restTemplate.postForEntity(apiUrl + "/distribute", payload, Map.class); + + // Mock ì‘답 + String distributionId = "UDTV-" + UUID.randomUUID().toString(); + int estimatedReach = 5000; + + return ChannelDistributionResult.builder() + .channel(ChannelType.URIDONGNETV) + .success(true) + .distributionId(distributionId) + .estimatedReach(estimatedReach) + .build(); + } + + private String getChannelSetting(DistributionRequest request, String key, String defaultValue) { + if (request.getChannelSettings() != null) { + Map settings = request.getChannelSettings().get(ChannelType.URIDONGNETV.name()); + if (settings != null && settings.containsKey(key)) { + return settings.get(key).toString(); + } + } + return defaultValue; + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/config/KafkaConfig.java b/distribution-service/src/main/java/com/kt/distribution/config/KafkaConfig.java new file mode 100644 index 0000000..92f2c90 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/config/KafkaConfig.java @@ -0,0 +1,46 @@ +package com.kt.distribution.config; + +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Kafka Configuration + * Kafka Producer 설정 + * + * @author System Architect + * @since 2025-10-23 + */ +@Configuration +@ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = true) +public class KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + configProps.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false); + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/config/WebConfig.java b/distribution-service/src/main/java/com/kt/distribution/config/WebConfig.java new file mode 100644 index 0000000..1b7c1d0 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/config/WebConfig.java @@ -0,0 +1,32 @@ +package com.kt.distribution.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Web Configuration + * CORS 설정 ë° ê¸°íƒ€ 웹 관련 설정 + * + * @author System Architect + * @since 2025-10-24 + */ +@Configuration +public class WebConfig implements WebMvcConfigurer { + + /** + * CORS 설정 + * - 모든 origin 허용 (개발 환경) + * - 모든 HTTP 메서드 허용 + * - Credentials 허용 + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/controller/DistributionController.java b/distribution-service/src/main/java/com/kt/distribution/controller/DistributionController.java new file mode 100644 index 0000000..c17503f --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/controller/DistributionController.java @@ -0,0 +1,71 @@ +package com.kt.distribution.controller; + +import com.kt.distribution.dto.DistributionRequest; +import com.kt.distribution.dto.DistributionResponse; +import com.kt.distribution.dto.DistributionStatusResponse; +import com.kt.distribution.service.DistributionService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * Distribution Controller + * POST /api/distribution/distribute - 다중 ì±„ë„ ë°°í¬ ì‹¤í–‰ + * GET /api/distribution/{eventId}/status - ë°°í¬ ìƒíƒœ 조회 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@RestController +@RequestMapping("/api/distribution") +@RequiredArgsConstructor +public class DistributionController { + + private final DistributionService distributionService; + + /** + * 다중 ì±„ë„ ë°°í¬ ì‹¤í–‰ + * UFR-DIST-010: 다중채ë„ë°°í¬ + * + * @param request DistributionRequest + * @return DistributionResponse + */ + @PostMapping("/distribute") + public ResponseEntity distribute(@RequestBody DistributionRequest request) { + log.info("Received distribution request: eventId={}, channels={}", + request.getEventId(), request.getChannels()); + + DistributionResponse response = distributionService.distribute(request); + + log.info("Distribution request processed: eventId={}, success={}, successCount={}, failureCount={}", + response.getEventId(), response.isSuccess(), + response.getSuccessCount(), response.getFailureCount()); + + return ResponseEntity.ok(response); + } + + /** + * ë°°í¬ ìƒíƒœ 조회 + * UFR-DIST-020: ë°°í¬ìƒíƒœì¡°íšŒ + * + * @param eventId ì´ë²¤íЏ ID + * @return DistributionStatusResponse + */ + @GetMapping("/{eventId}/status") + public ResponseEntity getDistributionStatus(@PathVariable String eventId) { + log.info("Received distribution status request: eventId={}", eventId); + + DistributionStatusResponse response = distributionService.getDistributionStatus(eventId); + + log.info("Distribution status retrieved: eventId={}, overallStatus={}", + eventId, response.getOverallStatus()); + + if ("NOT_FOUND".equals(response.getOverallStatus())) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(response); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/ChannelDistributionResult.java b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelDistributionResult.java new file mode 100644 index 0000000..915cfa1 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelDistributionResult.java @@ -0,0 +1,49 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 채ë„별 ë°°í¬ ê²°ê³¼ + * + * @author System Architect + * @since 2025-10-23 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelDistributionResult { + + /** + * ì±„ë„ íƒ€ìž… + */ + private ChannelType channel; + + /** + * ë°°í¬ ì„±ê³µ 여부 + */ + private boolean success; + + /** + * ë°°í¬ ID (성공 시) + */ + private String distributionId; + + /** + * ì˜ˆìƒ ë…¸ì¶œ 수 (성공 시) + */ + private Integer estimatedReach; + + /** + * ì—러 메시지 (실패 시) + */ + private String errorMessage; + + /** + * ë°°í¬ ì†Œìš” 시간 (ms) + */ + private long executionTimeMs; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/ChannelStatus.java b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelStatus.java new file mode 100644 index 0000000..a65567d --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelStatus.java @@ -0,0 +1,100 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 채ë„별 ë°°í¬ ìƒíƒœ DTO + * + * ê° ì±„ë„ì˜ ë°°í¬ ì§„í–‰ ìƒíƒœ ë° ê²°ê³¼ 정보를 담습니다. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelStatus { + + /** + * ì±„ë„ íƒ€ìž… + */ + private ChannelType channel; + + /** + * 채ë„별 ë°°í¬ ìƒíƒœ + * - PENDING: 대기 중 + * - IN_PROGRESS: ì§„í–‰ 중 + * - COMPLETED: 완료 + * - FAILED: 실패 + */ + private String status; + + /** + * 진행률 (0-100, IN_PROGRESS ìƒíƒœì¼ 때 사용) + */ + private Integer progress; + + /** + * 채ë„별 ë°°í¬ ID (우리ë™ë„¤TV, 지니TV 등) + */ + private String distributionId; + + /** + * ì˜ˆìƒ ë…¸ì¶œ 수 (우리ë™ë„¤TV, 지니TV) + */ + private Integer estimatedViews; + + /** + * ì—…ë°ì´íЏ 완료 ì‹œê° (ë§ê³ ë¹„즈) + */ + private LocalDateTime updateTimestamp; + + /** + * ê´‘ê³  ID (지니TV) + */ + private String adId; + + /** + * 노출 스케줄 (지니TV) + */ + private List impressionSchedule; + + /** + * 게시물 URL (Instagram, Naver Blog) + */ + private String postUrl; + + /** + * 게시물 ID (Instagram) + */ + private String postId; + + /** + * 메시지 ID (Kakao Channel) + */ + private String messageId; + + /** + * 완료 ì‹œê° + */ + private LocalDateTime completedAt; + + /** + * 오류 메시지 (실패 시) + */ + private String errorMessage; + + /** + * ìž¬ì‹œë„ íšŸìˆ˜ + */ + private Integer retries; + + /** + * 마지막 ìž¬ì‹œë„ ì‹œê° + */ + private LocalDateTime lastRetryAt; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/ChannelType.java b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelType.java new file mode 100644 index 0000000..8a92b3b --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/ChannelType.java @@ -0,0 +1,26 @@ +package com.kt.distribution.dto; + +/** + * ë°°í¬ ì±„ë„ íƒ€ìž… + * + * @author System Architect + * @since 2025-10-23 + */ +public enum ChannelType { + URIDONGNETV("우리ë™ë„¤TV"), + RINGOBIZ("ë§ê³ ë¹„즈"), + GINITV("지니TV"), + INSTAGRAM("Instagram"), + NAVER("Naver Blog"), + KAKAO("Kakao Channel"); + + private final String displayName; + + ChannelType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/DistributionRequest.java b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionRequest.java new file mode 100644 index 0000000..fa1f68f --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionRequest.java @@ -0,0 +1,54 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * ë°°í¬ ìš”ì²­ DTO + * POST /api/distribution/distribute + * + * @author System Architect + * @since 2025-10-23 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DistributionRequest { + + /** + * ì´ë²¤íЏ ID + */ + private String eventId; + + /** + * ì´ë²¤íЏ 제목 + */ + private String title; + + /** + * ì´ë²¤íЏ 설명 + */ + private String description; + + /** + * ì´ë¯¸ì§€ URL (CDN) + */ + private String imageUrl; + + /** + * ë°°í¬í•  ì±„ë„ ëª©ë¡ + */ + private List channels; + + /** + * 채ë„별 추가 설정 (Optional) + * 예: { "URIDONGNETV": { "radius": "1km", "timeSlot": "evening" } } + */ + private Map> channelSettings; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/DistributionResponse.java b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionResponse.java new file mode 100644 index 0000000..9945d80 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionResponse.java @@ -0,0 +1,63 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * ë°°í¬ ì‘답 DTO + * POST /api/distribution/distribute + * + * @author System Architect + * @since 2025-10-23 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DistributionResponse { + + /** + * ì´ë²¤íЏ ID + */ + private String eventId; + + /** + * ë°°í¬ ì„±ê³µ 여부 (모든 ì±„ë„ ë˜ëŠ” ì¼ë¶€ ì±„ë„ ì„±ê³µ) + */ + private boolean success; + + /** + * 채ë„별 ë°°í¬ ê²°ê³¼ + */ + private List channelResults; + + /** + * 성공한 ì±„ë„ ìˆ˜ + */ + private int successCount; + + /** + * 실패한 ì±„ë„ ìˆ˜ + */ + private int failureCount; + + /** + * ë°°í¬ ì™„ë£Œ ì‹œê° + */ + private LocalDateTime completedAt; + + /** + * ì „ì²´ ë°°í¬ ì†Œìš” 시간 (ms) + */ + private long totalExecutionTimeMs; + + /** + * 메시지 + */ + private String message; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/dto/DistributionStatusResponse.java b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionStatusResponse.java new file mode 100644 index 0000000..f65e964 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/dto/DistributionStatusResponse.java @@ -0,0 +1,52 @@ +package com.kt.distribution.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * ë°°í¬ ìƒíƒœ 조회 ì‘답 DTO + * + * 특정 ì´ë²¤íŠ¸ì˜ ì „ì²´ ë°°í¬ ìƒíƒœ ë° ì±„ë„별 ìƒì„¸ ìƒíƒœ 정보를 담습니다. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DistributionStatusResponse { + + /** + * ì´ë²¤íЏ ID + */ + private String eventId; + + /** + * ì „ì²´ ë°°í¬ ìƒíƒœ + * - PENDING: 대기 중 + * - IN_PROGRESS: ì§„í–‰ 중 + * - COMPLETED: 완료 + * - PARTIAL_FAILURE: 부분 성공 + * - FAILED: 실패 + * - NOT_FOUND: ë°°í¬ ì´ë ¥ ì—†ìŒ + */ + private String overallStatus; + + /** + * ë°°í¬ ì‹œìž‘ ì‹œê° + */ + private LocalDateTime startedAt; + + /** + * ë°°í¬ ì™„ë£Œ ì‹œê° + */ + private LocalDateTime completedAt; + + /** + * 채ë„별 ë°°í¬ ìƒíƒœ ëª©ë¡ + */ + private List channels; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/event/DistributionCompletedEvent.java b/distribution-service/src/main/java/com/kt/distribution/event/DistributionCompletedEvent.java new file mode 100644 index 0000000..467b6a6 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/event/DistributionCompletedEvent.java @@ -0,0 +1,48 @@ +package com.kt.distribution.event; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Distribution Completed Event + * ë°°í¬ ì™„ë£Œ 시 Kafka로 발행하는 ì´ë²¤íЏ + * + * @author System Architect + * @since 2025-10-23 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DistributionCompletedEvent { + + /** + * ì´ë²¤íЏ ID + */ + private String eventId; + + /** + * ë°°í¬ ì™„ë£Œëœ ì±„ë„ ëª©ë¡ + */ + private List distributedChannels; + + /** + * ë°°í¬ ì™„ë£Œ ì‹œê° + */ + private LocalDateTime completedAt; + + /** + * 성공한 ì±„ë„ ìˆ˜ + */ + private int successCount; + + /** + * 실패한 ì±„ë„ ìˆ˜ + */ + private int failureCount; +} diff --git a/distribution-service/src/main/java/com/kt/distribution/repository/DistributionStatusRepository.java b/distribution-service/src/main/java/com/kt/distribution/repository/DistributionStatusRepository.java new file mode 100644 index 0000000..aaa0b71 --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/repository/DistributionStatusRepository.java @@ -0,0 +1,65 @@ +package com.kt.distribution.repository; + +import com.kt.distribution.dto.DistributionStatusResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * ë°°í¬ ìƒíƒœ 저장소 + * + * 메모리 기반으로 ë°°í¬ ìƒíƒœë¥¼ 관리합니다. + * 실제 ìš´ì˜ í™˜ê²½ì—서는 Redis ë˜ëŠ” ë°ì´í„°ë² ì´ìŠ¤ë¥¼ 사용하여 ì˜êµ¬ 저장하는 ê²ƒì„ ê¶Œìž¥í•©ë‹ˆë‹¤. + */ +@Slf4j +@Repository +public class DistributionStatusRepository { + + /** + * ì´ë²¤íЏ ID를 키로 ë°°í¬ ìƒíƒœë¥¼ 저장하는 메모리 저장소 + */ + private final Map distributionStatuses = new ConcurrentHashMap<>(); + + /** + * ë°°í¬ ìƒíƒœ 저장 + * + * @param eventId ì´ë²¤íЏ ID + * @param status ë°°í¬ ìƒíƒœ + */ + public void save(String eventId, DistributionStatusResponse status) { + log.debug("Saving distribution status: eventId={}, overallStatus={}", eventId, status.getOverallStatus()); + distributionStatuses.put(eventId, status); + } + + /** + * ë°°í¬ ìƒíƒœ 조회 + * + * @param eventId ì´ë²¤íЏ ID + * @return ë°°í¬ ìƒíƒœ (없으면 Optional.empty()) + */ + public Optional findByEventId(String eventId) { + log.debug("Finding distribution status: eventId={}", eventId); + return Optional.ofNullable(distributionStatuses.get(eventId)); + } + + /** + * ë°°í¬ ìƒíƒœ ì‚­ì œ + * + * @param eventId ì´ë²¤íЏ ID + */ + public void delete(String eventId) { + log.debug("Deleting distribution status: eventId={}", eventId); + distributionStatuses.remove(eventId); + } + + /** + * 모든 ë°°í¬ ìƒíƒœ ì‚­ì œ (테스트용) + */ + public void deleteAll() { + log.debug("Deleting all distribution statuses"); + distributionStatuses.clear(); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/service/DistributionService.java b/distribution-service/src/main/java/com/kt/distribution/service/DistributionService.java new file mode 100644 index 0000000..dac436f --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/service/DistributionService.java @@ -0,0 +1,261 @@ +package com.kt.distribution.service; + +import com.kt.distribution.adapter.ChannelAdapter; +import com.kt.distribution.dto.*; +import com.kt.distribution.event.DistributionCompletedEvent; +import com.kt.distribution.repository.DistributionStatusRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +/** + * Distribution Service + * 다중 ì±„ë„ ë³‘ë ¬ ë°°í¬ ë° Kafka Event 발행 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Service +public class DistributionService { + + private final List channelAdapters; + private final Optional kafkaEventPublisher; + private final DistributionStatusRepository statusRepository; + + @Autowired + public DistributionService(List channelAdapters, + Optional kafkaEventPublisher, + DistributionStatusRepository statusRepository) { + this.channelAdapters = channelAdapters; + this.kafkaEventPublisher = kafkaEventPublisher; + this.statusRepository = statusRepository; + } + + // 병렬 ì‹¤í–‰ì„ ìœ„í•œ ExecutorService (채ë„별 스레드 í’€) + private final ExecutorService executorService = Executors.newFixedThreadPool(10); + + /** + * 다중 ì±„ë„ ë³‘ë ¬ ë°°í¬ + * + * @param request DistributionRequest + * @return DistributionResponse + */ + public DistributionResponse distribute(DistributionRequest request) { + LocalDateTime startedAt = LocalDateTime.now(); + long startTime = System.currentTimeMillis(); + + log.info("Starting multi-channel distribution: eventId={}, channels={}", + request.getEventId(), request.getChannels()); + + // ë°°í¬ ì‹œìž‘ ìƒíƒœ 저장 (IN_PROGRESS) + saveInProgressStatus(request.getEventId(), request.getChannels(), startedAt); + + // ì±„ë„ ì–´ëŒ‘í„° 매핑 (타입별) + Map adapterMap = channelAdapters.stream() + .collect(Collectors.toMap( + adapter -> adapter.getChannelType().name(), + adapter -> adapter + )); + + // 병렬 ë°°í¬ ì‹¤í–‰ + List> futures = request.getChannels().stream() + .map(channelType -> { + ChannelAdapter adapter = adapterMap.get(channelType.name()); + if (adapter == null) { + log.warn("No adapter found for channel: {}", channelType); + return CompletableFuture.completedFuture( + ChannelDistributionResult.builder() + .channel(channelType) + .success(false) + .errorMessage("Adapter not found") + .build() + ); + } + + // 비ë™ê¸° 실행 + return CompletableFuture.supplyAsync( + () -> adapter.distribute(request), + executorService + ); + }) + .collect(Collectors.toList()); + + // 모든 ë°°í¬ ì™„ë£Œ 대기 + CompletableFuture allOf = CompletableFuture.allOf( + futures.toArray(new CompletableFuture[0]) + ); + + allOf.join(); // 블로킹 대기 (최대 1ë¶„ 목표) + + // ê²°ê³¼ 수집 + List results = futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()); + + long totalExecutionTime = System.currentTimeMillis() - startTime; + LocalDateTime completedAt = LocalDateTime.now(); + + // 성공/실패 카운트 + long successCount = results.stream().filter(ChannelDistributionResult::isSuccess).count(); + long failureCount = results.size() - successCount; + + log.info("Multi-channel distribution completed: eventId={}, successCount={}, failureCount={}, totalTime={}ms", + request.getEventId(), successCount, failureCount, totalExecutionTime); + + // ë°°í¬ ì™„ë£Œ ìƒíƒœ 저장 (COMPLETED/PARTIAL_FAILURE/FAILED) + saveCompletedStatus(request.getEventId(), results, startedAt, completedAt, successCount, failureCount); + + // Kafka Event 발행 + publishDistributionCompletedEvent(request.getEventId(), results); + + // ì‘답 ìƒì„± + return DistributionResponse.builder() + .eventId(request.getEventId()) + .success(successCount > 0) // 1ê°œ ì´ìƒ 성공하면 성공으로 간주 + .channelResults(results) + .successCount((int) successCount) + .failureCount((int) failureCount) + .completedAt(completedAt) + .totalExecutionTimeMs(totalExecutionTime) + .message(String.format("Distribution completed: %d succeeded, %d failed", + successCount, failureCount)) + .build(); + } + + /** + * ë°°í¬ ìƒíƒœ 조회 + * + * @param eventId ì´ë²¤íЏ ID + * @return ë°°í¬ ìƒíƒœ + */ + public DistributionStatusResponse getDistributionStatus(String eventId) { + return statusRepository.findByEventId(eventId) + .orElse(DistributionStatusResponse.builder() + .eventId(eventId) + .overallStatus("NOT_FOUND") + .channels(List.of()) + .build()); + } + + /** + * ë°°í¬ ì‹œìž‘ ìƒíƒœ 저장 (IN_PROGRESS) + * + * @param eventId ì´ë²¤íЏ ID + * @param channels ë°°í¬ ì±„ë„ ëª©ë¡ + * @param startedAt 시작 ì‹œê° + */ + private void saveInProgressStatus(String eventId, List channels, LocalDateTime startedAt) { + List channelStatuses = channels.stream() + .map(channelType -> ChannelStatus.builder() + .channel(channelType) + .status("PENDING") + .build()) + .collect(Collectors.toList()); + + DistributionStatusResponse status = DistributionStatusResponse.builder() + .eventId(eventId) + .overallStatus("IN_PROGRESS") + .startedAt(startedAt) + .channels(channelStatuses) + .build(); + + statusRepository.save(eventId, status); + } + + /** + * ë°°í¬ ì™„ë£Œ ìƒíƒœ 저장 + * + * @param eventId ì´ë²¤íЏ ID + * @param results ë°°í¬ ê²°ê³¼ + * @param startedAt 시작 ì‹œê° + * @param completedAt 완료 ì‹œê° + * @param successCount 성공 개수 + * @param failureCount 실패 개수 + */ + private void saveCompletedStatus(String eventId, List results, + LocalDateTime startedAt, LocalDateTime completedAt, + long successCount, long failureCount) { + // ì „ì²´ ìƒíƒœ ê²°ì • + String overallStatus; + if (successCount == 0) { + overallStatus = "FAILED"; + } else if (failureCount == 0) { + overallStatus = "COMPLETED"; + } else { + overallStatus = "PARTIAL_FAILURE"; + } + + // ChannelDistributionResult → ChannelStatus 변환 + List channelStatuses = results.stream() + .map(this::convertToChannelStatus) + .collect(Collectors.toList()); + + DistributionStatusResponse status = DistributionStatusResponse.builder() + .eventId(eventId) + .overallStatus(overallStatus) + .startedAt(startedAt) + .completedAt(completedAt) + .channels(channelStatuses) + .build(); + + statusRepository.save(eventId, status); + } + + /** + * ChannelDistributionResult를 ChannelStatus로 변환 + * + * @param result ë°°í¬ ê²°ê³¼ + * @return ì±„ë„ ìƒíƒœ + */ + private ChannelStatus convertToChannelStatus(ChannelDistributionResult result) { + return ChannelStatus.builder() + .channel(result.getChannel()) + .status(result.isSuccess() ? "COMPLETED" : "FAILED") + .distributionId(result.getDistributionId()) + .estimatedViews(result.getEstimatedReach()) + .completedAt(LocalDateTime.now()) + .errorMessage(result.getErrorMessage()) + .build(); + } + + /** + * DistributionCompleted ì´ë²¤íЏ 발행 + * + * @param eventId ì´ë²¤íЏ ID + * @param results 채ë„별 ë°°í¬ ê²°ê³¼ + */ + private void publishDistributionCompletedEvent(String eventId, List results) { + if (kafkaEventPublisher.isEmpty()) { + log.warn("KafkaEventPublisher not available - skipping event publishing"); + return; + } + + List distributedChannels = results.stream() + .filter(ChannelDistributionResult::isSuccess) + .map(result -> result.getChannel().name()) + .collect(Collectors.toList()); + + long successCount = results.stream().filter(ChannelDistributionResult::isSuccess).count(); + long failureCount = results.size() - successCount; + + DistributionCompletedEvent event = DistributionCompletedEvent.builder() + .eventId(eventId) + .distributedChannels(distributedChannels) + .completedAt(LocalDateTime.now()) + .successCount((int) successCount) + .failureCount((int) failureCount) + .build(); + + kafkaEventPublisher.get().publishDistributionCompleted(event); + } +} diff --git a/distribution-service/src/main/java/com/kt/distribution/service/KafkaEventPublisher.java b/distribution-service/src/main/java/com/kt/distribution/service/KafkaEventPublisher.java new file mode 100644 index 0000000..5e90b3c --- /dev/null +++ b/distribution-service/src/main/java/com/kt/distribution/service/KafkaEventPublisher.java @@ -0,0 +1,62 @@ +package com.kt.distribution.service; + +import com.kt.distribution.event.DistributionCompletedEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Service; + +import java.util.concurrent.CompletableFuture; + +/** + * Kafka Event Publisher + * DistributionCompleted ì´ë²¤íŠ¸ë¥¼ Kafka로 발행 + * + * @author System Architect + * @since 2025-10-23 + */ +@Slf4j +@Service +@ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = true) +@RequiredArgsConstructor +public class KafkaEventPublisher { + + private final KafkaTemplate kafkaTemplate; + + @Value("${kafka.topics.distribution-completed}") + private String distributionCompletedTopic; + + /** + * ë°°í¬ ì™„ë£Œ ì´ë²¤íЏ 발행 + * + * @param event DistributionCompletedEvent + */ + public void publishDistributionCompleted(DistributionCompletedEvent event) { + try { + log.info("Publishing DistributionCompletedEvent: eventId={}, successCount={}, failureCount={}", + event.getEventId(), event.getSuccessCount(), event.getFailureCount()); + + CompletableFuture> future = + kafkaTemplate.send(distributionCompletedTopic, event.getEventId(), event); + + future.whenComplete((result, ex) -> { + if (ex == null) { + log.info("DistributionCompletedEvent published successfully: topic={}, partition={}, offset={}", + distributionCompletedTopic, + result.getRecordMetadata().partition(), + result.getRecordMetadata().offset()); + } else { + log.error("Failed to publish DistributionCompletedEvent: eventId={}, error={}", + event.getEventId(), ex.getMessage(), ex); + } + }); + + } catch (Exception e) { + log.error("Error publishing DistributionCompletedEvent: eventId={}, error={}", + event.getEventId(), e.getMessage(), e); + } + } +} diff --git a/distribution-service/src/main/resources/application.yml b/distribution-service/src/main/resources/application.yml new file mode 100644 index 0000000..5013aa7 --- /dev/null +++ b/distribution-service/src/main/resources/application.yml @@ -0,0 +1,102 @@ +server: + port: 8085 + +spring: + application: + name: distribution-service + + # Disable auto-configuration (No database required) + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration + - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration + - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration + + kafka: + enabled: ${KAFKA_ENABLED:true} + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:4.230.50.63:9092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + properties: + spring.json.type.mapping: distributionCompleted:com.kt.distribution.event.DistributionCompletedEvent + consumer: + group-id: ${KAFKA_CONSUMER_GROUP:distribution-service} + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: '*' + +# Kafka Topics +kafka: + topics: + distribution-completed: distribution-completed + +# Resilience4j Configuration +resilience4j: + circuitbreaker: + instances: + channelApi: + failure-rate-threshold: 50 + slow-call-rate-threshold: 50 + slow-call-duration-threshold: 5000ms + wait-duration-in-open-state: 30s + permitted-number-of-calls-in-half-open-state: 3 + sliding-window-type: COUNT_BASED + sliding-window-size: 10 + minimum-number-of-calls: 5 + + retry: + instances: + channelApi: + max-attempts: 3 + wait-duration: 1s + exponential-backoff-multiplier: 2 + retry-exceptions: + - java.net.SocketTimeoutException + - java.net.ConnectException + - org.springframework.web.client.ResourceAccessException + + bulkhead: + instances: + channelApi: + max-concurrent-calls: 10 + max-wait-duration: 0ms + +# External Channel APIs (Mock URLs) +channel: + apis: + uridongnetv: + url: ${URIDONGNETV_API_URL:http://localhost:9001/api/uridongnetv} + timeout: 10000 + ringobiz: + url: ${RINGOBIZ_API_URL:http://localhost:9002/api/ringobiz} + timeout: 10000 + ginitv: + url: ${GINITV_API_URL:http://localhost:9003/api/ginitv} + timeout: 10000 + instagram: + url: ${INSTAGRAM_API_URL:http://localhost:9004/api/instagram} + timeout: 10000 + naver: + url: ${NAVER_API_URL:http://localhost:9005/api/naver} + timeout: 10000 + kakao: + url: ${KAKAO_API_URL:http://localhost:9006/api/kakao} + timeout: 10000 + +# Logging +logging: + file: + name: ${LOG_FILE:logs/distribution-service.log} + logback: + rollingpolicy: + max-file-size: 10MB + max-history: 7 + total-size-cap: 100MB + level: + com.kt.distribution: DEBUG + org.springframework.kafka: INFO + io.github.resilience4j: DEBUG diff --git a/distribution-service/src/main/resources/mock-events.json b/distribution-service/src/main/resources/mock-events.json new file mode 100644 index 0000000..8e086a8 --- /dev/null +++ b/distribution-service/src/main/resources/mock-events.json @@ -0,0 +1,134 @@ +[ + { + "eventId": "evt-test-001", + "title": "ë´„ë§žì´ ì‚¼ê²¹ì‚´ 50% í• ì¸ ì´ë²¤íЏ", + "description": "3ì›” 한정 특별 ì´ë²¤íЏ! 삼겹살 1ì¸ë¶„ 무료 ì¦ì •", + "imageUrl": "https://cdn.example.com/event-image-001.jpg", + "channels": ["URIDONGNETV", "INSTAGRAM", "KAKAO", "NAVER"], + "channelSettings": { + "URIDONGNETV": { + "radius": "1km", + "timeSlot": "evening" + } + } + }, + { + "eventId": "evt-test-002", + "title": "ì‹ ê·œ ê³ ê° í™˜ì˜! 치킨 3,000ì› í• ì¸", + "description": "ì²˜ìŒ ë°©ë¬¸í•˜ì‹œëŠ” ê³ ê°ë‹˜ê»˜ 특별 í• ì¸ ì¿ í° ì œê³µ. 기간 ë‚´ 사용 가능", + "imageUrl": "https://cdn.example.com/event-image-002.jpg", + "channels": ["INSTAGRAM", "KAKAO", "NAVER"], + "channelSettings": { + "INSTAGRAM": { + "hashtags": ["치킨", "í• ì¸", "신규고ê°"] + } + } + }, + { + "eventId": "evt-test-003", + "title": "ì£¼ë§ íŠ¹ê°€! í”¼ìž 1+1 ì´ë²¤íЏ", + "description": "토요ì¼, ì¼ìš”ì¼ í•œì •! 모든 í”¼ìž 1+1 행사. 배달 주문 가능", + "imageUrl": "https://cdn.example.com/event-image-003.jpg", + "channels": ["URIDONGNETV", "KAKAO"], + "channelSettings": { + "URIDONGNETV": { + "radius": "2km", + "timeSlot": "lunch" + }, + "KAKAO": { + "targetAge": "20-40", + "sendTime": "11:00" + } + } + }, + { + "eventId": "evt-test-004", + "title": "여름 시즌 냉면 페스티벌", + "description": "시ì›í•œ 냉면과 함께하는 여름! ì „ 메뉴 20% í• ì¸", + "imageUrl": "https://cdn.example.com/event-image-004.jpg", + "channels": ["URIDONGNETV", "INSTAGRAM", "NAVER"], + "channelSettings": { + "URIDONGNETV": { + "radius": "500m", + "timeSlot": "afternoon" + } + } + }, + { + "eventId": "evt-test-005", + "title": "리뷰 작성 시 ìŒë£Œ 무료!", + "description": "네ì´ë²„ 리뷰 작성하고 아메리카노 1ìž” 무료로 받아가세요", + "imageUrl": "https://cdn.example.com/event-image-005.jpg", + "channels": ["NAVER", "INSTAGRAM"], + "channelSettings": { + "NAVER": { + "reviewRequired": true + } + } + }, + { + "eventId": "evt-test-006", + "title": "ìƒì¼ 축하! ì¼€ì´í¬ 30% í• ì¸", + "description": "ìƒì¼ ë‹¹ì¼ ë°©ë¬¸ 시 ì‹ ë¶„ì¦ ì œì‹œí•˜ë©´ ì¼€ì´í¬ 30% í• ì¸. 예약 필수", + "imageUrl": "https://cdn.example.com/event-image-006.jpg", + "channels": ["KAKAO", "INSTAGRAM"], + "channelSettings": { + "KAKAO": { + "reservationRequired": true, + "targetAge": "all" + } + } + }, + { + "eventId": "evt-test-007", + "title": "ì ì‹¬ 시간 특가! 런치 세트 8,000ì›", + "description": "í‰ì¼ 11:30~14:00 런치 세트 메뉴 8,000ì›. 커피 í¬í•¨", + "imageUrl": "https://cdn.example.com/event-image-007.jpg", + "channels": ["URIDONGNETV", "NAVER"], + "channelSettings": { + "URIDONGNETV": { + "radius": "1.5km", + "timeSlot": "lunch" + } + } + }, + { + "eventId": "evt-test-008", + "title": "가족 ë‚˜ë“¤ì´ íŒ¨í‚¤ì§€ 20% í• ì¸", + "description": "4ì¸ ê°€ì¡± 세트 메뉴 20% í• ì¸! 키즈 메뉴 í¬í•¨", + "imageUrl": "https://cdn.example.com/event-image-008.jpg", + "channels": ["KAKAO", "INSTAGRAM", "NAVER"], + "channelSettings": { + "KAKAO": { + "targetAge": "30-50", + "sendTime": "10:00" + } + } + }, + { + "eventId": "evt-test-009", + "title": "야간 í• ì¸! ì €ë… 9시 ì´í›„ ì „ 메뉴 15% OFF", + "description": "ì €ë… 9시 ì´í›„ 방문 시 모든 메뉴 15% í• ì¸. í¬ìž¥ 가능", + "imageUrl": "https://cdn.example.com/event-image-009.jpg", + "channels": ["URIDONGNETV", "INSTAGRAM"], + "channelSettings": { + "URIDONGNETV": { + "radius": "1km", + "timeSlot": "evening" + } + } + }, + { + "eventId": "evt-test-010", + "title": "SNS 팔로우 ì´ë²¤íЏ! 디저트 무료", + "description": "ì¸ìŠ¤íƒ€ê·¸ëž¨ 팔로우 후 ì¸ì¦í•˜ë©´ 디저트 1ê°œ 무료 제공", + "imageUrl": "https://cdn.example.com/event-image-010.jpg", + "channels": ["INSTAGRAM", "KAKAO"], + "channelSettings": { + "INSTAGRAM": { + "followRequired": true, + "hashtags": ["팔로우ì´ë²¤íЏ", "디저트무료", "맛집"] + } + } + } +] diff --git a/distribution-service/test-distribution.sh b/distribution-service/test-distribution.sh new file mode 100644 index 0000000..90d5f13 --- /dev/null +++ b/distribution-service/test-distribution.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Distribution Service API 테스트 스í¬ë¦½íЏ + +echo "=== Distribution Service API Test ===" +echo "" + +# 1. Health Check (추후 추가 예정) +# echo "1. Health Check..." +# curl -X GET http://localhost:8085/actuator/health +# echo "" + +# 2. 다중 ì±„ë„ ë°°í¬ í…ŒìŠ¤íŠ¸ +echo "1. Testing Multi-Channel Distribution..." +echo "" + +curl -X POST http://localhost:8085/api/distribution/distribute \ + -H "Content-Type: application/json" \ + -d '{ + "eventId": "evt-test-001", + "title": "ë´„ë§žì´ ì‚¼ê²¹ì‚´ 50% í• ì¸ ì´ë²¤íЏ", + "description": "3ì›” 한정 특별 ì´ë²¤íЏ! 삼겹살 1ì¸ë¶„ 무료 ì¦ì •", + "imageUrl": "https://cdn.example.com/event-image.jpg", + "channels": ["URIDONGNETV", "INSTAGRAM", "KAKAO", "NAVER"], + "channelSettings": { + "URIDONGNETV": { + "radius": "1km", + "timeSlot": "evening" + } + } + }' | jq '.' + +echo "" +echo "=== Test Completed ===" diff --git a/tools/run-intellij-service-profile.py b/tools/run-intellij-service-profile.py new file mode 100644 index 0000000..2278686 --- /dev/null +++ b/tools/run-intellij-service-profile.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Tripgen Service Runner Script +Reads execution profiles from {service-name}/.run/{service-name}.run.xml and runs services accordingly. + +Usage: + python run-config.py + +Examples: + python run-config.py user-service + python run-config.py location-service + python run-config.py trip-service + python run-config.py ai-service +""" + +import os +import sys +import subprocess +import xml.etree.ElementTree as ET +from pathlib import Path +import argparse + + +def get_project_root(): + """Find project root directory""" + current_dir = Path(__file__).parent.absolute() + while current_dir.parent != current_dir: + if (current_dir / 'gradlew').exists() or (current_dir / 'gradlew.bat').exists(): + return current_dir + current_dir = current_dir.parent + + # If gradlew not found, assume parent directory of develop as project root + return Path(__file__).parent.parent.absolute() + + +def parse_run_configurations(project_root, service_name=None): + """Parse run configuration files from .run directories""" + configurations = {} + + if service_name: + # Parse specific service configuration + run_config_path = project_root / service_name / '.run' / f'{service_name}.run.xml' + if run_config_path.exists(): + config = parse_single_run_config(run_config_path, service_name) + if config: + configurations[service_name] = config + else: + print(f"[ERROR] Cannot find run configuration: {run_config_path}") + else: + # Find all service directories + service_dirs = ['user-service', 'location-service', 'trip-service', 'ai-service'] + for service in service_dirs: + run_config_path = project_root / service / '.run' / f'{service}.run.xml' + if run_config_path.exists(): + config = parse_single_run_config(run_config_path, service) + if config: + configurations[service] = config + + return configurations + + +def parse_single_run_config(config_path, service_name): + """Parse a single run configuration file""" + try: + tree = ET.parse(config_path) + root = tree.getroot() + + # Find configuration element + config = root.find('.//configuration[@type="GradleRunConfiguration"]') + if config is None: + print(f"[WARNING] No Gradle configuration found in {config_path}") + return None + + # Extract environment variables + env_vars = {} + env_option = config.find('.//option[@name="env"]') + if env_option is not None: + env_map = env_option.find('map') + if env_map is not None: + for entry in env_map.findall('entry'): + key = entry.get('key') + value = entry.get('value') + if key and value: + env_vars[key] = value + + # Extract task names + task_names = [] + task_names_option = config.find('.//option[@name="taskNames"]') + if task_names_option is not None: + task_list = task_names_option.find('list') + if task_list is not None: + for option in task_list.findall('option'): + value = option.get('value') + if value: + task_names.append(value) + + if env_vars or task_names: + return { + 'env_vars': env_vars, + 'task_names': task_names, + 'config_path': str(config_path) + } + + return None + + except ET.ParseError as e: + print(f"[ERROR] XML parsing error in {config_path}: {e}") + return None + except Exception as e: + print(f"[ERROR] Error reading {config_path}: {e}") + return None + + +def get_gradle_command(project_root): + """Return appropriate Gradle command for OS""" + if os.name == 'nt': # Windows + gradle_bat = project_root / 'gradlew.bat' + if gradle_bat.exists(): + return str(gradle_bat) + return 'gradle.bat' + else: # Unix-like (Linux, macOS) + gradle_sh = project_root / 'gradlew' + if gradle_sh.exists(): + return str(gradle_sh) + return 'gradle' + + +def run_service(service_name, config, project_root): + """Run service""" + print(f"[START] Starting {service_name} service...") + + # Set environment variables + env = os.environ.copy() + for key, value in config['env_vars'].items(): + env[key] = value + print(f" [ENV] {key}={value}") + + # Prepare Gradle command + gradle_cmd = get_gradle_command(project_root) + + # Execute tasks + for task_name in config['task_names']: + print(f"\n[RUN] Executing: {task_name}") + + cmd = [gradle_cmd, task_name] + + try: + # Execute from project root directory + process = subprocess.Popen( + cmd, + cwd=project_root, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + bufsize=1, + encoding='utf-8', + errors='replace' + ) + + print(f"[CMD] Command: {' '.join(cmd)}") + print(f"[DIR] Working directory: {project_root}") + print("=" * 50) + + # Real-time output + for line in process.stdout: + print(line.rstrip()) + + # Wait for process completion + process.wait() + + if process.returncode == 0: + print(f"\n[SUCCESS] {task_name} execution completed") + else: + print(f"\n[FAILED] {task_name} execution failed (exit code: {process.returncode})") + return False + + except KeyboardInterrupt: + print(f"\n[STOP] Interrupted by user") + process.terminate() + return False + except Exception as e: + print(f"\n[ERROR] Execution error: {e}") + return False + + return True + + +def list_available_services(configurations): + """List available services""" + print("[LIST] Available services:") + print("=" * 40) + + for service_name, config in configurations.items(): + if config['task_names']: + print(f" [SERVICE] {service_name}") + if 'config_path' in config: + print(f" +-- Config: {config['config_path']}") + for task in config['task_names']: + print(f" +-- Task: {task}") + print(f" +-- {len(config['env_vars'])} environment variables") + print() + + +def main(): + """Main function""" + parser = argparse.ArgumentParser( + description='Tripgen Service Runner Script', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python run-config.py user-service + python run-config.py location-service + python run-config.py trip-service + python run-config.py ai-service + python run-config.py --list + """ + ) + + parser.add_argument( + 'service_name', + nargs='?', + help='Service name to run' + ) + + parser.add_argument( + '--list', '-l', + action='store_true', + help='List available services' + ) + + args = parser.parse_args() + + # Find project root + project_root = get_project_root() + print(f"[INFO] Project root: {project_root}") + + # Parse run configurations + print("[INFO] Reading run configuration files...") + configurations = parse_run_configurations(project_root) + + if not configurations: + print("[ERROR] No execution configurations found") + return 1 + + print(f"[INFO] Found {len(configurations)} execution configurations") + + # List services request + if args.list: + list_available_services(configurations) + return 0 + + # If service name not provided + if not args.service_name: + print("\n[ERROR] Please provide service name") + list_available_services(configurations) + print("Usage: python run-config.py ") + return 1 + + # Find service + service_name = args.service_name + + # Try to parse specific service configuration if not found + if service_name not in configurations: + print(f"[INFO] Trying to find configuration for '{service_name}'...") + configurations = parse_run_configurations(project_root, service_name) + + if service_name not in configurations: + print(f"[ERROR] Cannot find '{service_name}' service") + list_available_services(configurations) + return 1 + + config = configurations[service_name] + + if not config['task_names']: + print(f"[ERROR] No executable tasks found for '{service_name}' service") + return 1 + + # Execute service + print(f"\n[TARGET] Starting '{service_name}' service execution") + print("=" * 50) + + success = run_service(service_name, config, project_root) + + if success: + print(f"\n[COMPLETE] '{service_name}' service started successfully!") + return 0 + else: + print(f"\n[FAILED] Failed to start '{service_name}' service") + return 1 + + +if __name__ == '__main__': + try: + exit_code = main() + sys.exit(exit_code) + except KeyboardInterrupt: + print("\n[STOP] Interrupted by user") + sys.exit(1) + except Exception as e: + print(f"\n[ERROR] Unexpected error occurred: {e}") + sys.exit(1) \ No newline at end of file From 9f50c7feaaf22277f7a7acfa488fd12f83d59e6b Mon Sep 17 00:00:00 2001 From: sunmingLee <25thbam@gmail.com> Date: Fri, 24 Oct 2025 13:08:42 +0900 Subject: [PATCH 02/61] =?UTF-8?q?distribution-service=EC=97=90=20PostgreSQ?= =?UTF-8?q?L,=20Redis=20=EC=97=B0=EA=B2=B0=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - application.ymlì— DB ë° ìºì‹œ 설정 추가 - IntelliJ 실행 프로파ì¼ì— 환경 변수 설정 - Kafka 브로커 주소를 실제 서버로 변경 --- .run/DistributionServiceApplication.run.xml | 13 +++++- .../src/main/resources/application.yml | 40 +++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/.run/DistributionServiceApplication.run.xml b/.run/DistributionServiceApplication.run.xml index 664df2a..b023d5b 100644 --- a/.run/DistributionServiceApplication.run.xml +++ b/.run/DistributionServiceApplication.run.xml @@ -10,8 +10,19 @@ - + + + + + + + + + + + +

ìƒì„¸í•œ ì„¤ëª…ì´ í•„ìš”í•œ 경우 ì—¬ê¸°ì— ìž‘ì„±í•©ë‹ˆë‹¤.

- * - * @param paramName 파ë¼ë¯¸í„° 설명 - * @return 반환값 설명 - * @throws ExceptionType 예외 ìƒí™© 설명 - * @since 1.0 - * @author 작성ìžëª… - * @see 관련í´ëž˜ìФ#메서드 - */ -``` - -### 2. **주요 JavaDoc 태그** - -| 태그 | 설명 | 사용 위치 | -|------|------|-----------| -| `@param` | 메서드 파ë¼ë¯¸í„° 설명 | 메서드 | -| `@return` | 반환값 설명 | 메서드 | -| `@throws` | 예외 ìƒí™© 설명 | 메서드 | -| `@since` | ë„ìž… 버전 | í´ëž˜ìФ, 메서드 | -| `@author` | ìž‘ì„±ìž | í´ëž˜ìФ | -| `@version` | 버전 ì •ë³´ | í´ëž˜ìФ | -| `@see` | 관련 항목 참조 | 모든 ê³³ | -| `@apiNote` | API 사용 시 주ì˜ì‚¬í•­ | 메서드 | -| `@implNote` | 구현 관련 참고사항 | 메서드 | - -## 🎨 HTML 태그 활용 ê°€ì´ë“œ - -### 1. **HTML 태그 사용 ì´ìœ ** - -JavaDocì€ ì†ŒìŠ¤ì½”ë“œ 주ì„ì„ íŒŒì‹±í•˜ì—¬ **HTML í˜•íƒœì˜ API 문서**를 ìžë™ ìƒì„±í•©ë‹ˆë‹¤. HTML 태그를 사용하면: - -- **ê°€ë…성 í–¥ìƒ**: êµ¬ì¡°í™”ëœ ë¬¸ì„œë¡œ ì´í•´í•˜ê¸° 쉬움 -- **ìžë™ 문서화**: JavaDoc ë„구가 ì˜ˆìœ HTML 문서 ìƒì„± -- **IDE ì§€ì›**: 개발 ë„구ì—서 리치 í…스트로 표시 -- **표준 준수**: Oracle JavaDoc ìŠ¤íƒ€ì¼ ê°€ì´ë“œ 준수 - -### 2. **ìžì£¼ 사용ë˜ëŠ” HTML 태그** - -#### **í…스트 서ì‹** -```java -/** - *

단ë½ì„ 구분할 때 사용합니다.

- * 중요한 ë‚´ìš©ì„ ê°•ì¡°í•  때 사용합니다. - * ì´íƒ¤ë¦­ì²´ë¡œ 표시할 때 사용합니다. - * method()와 ê°™ì€ ì½”ë“œë¥¼ 표시할 때 사용합니다. - */ -``` - -#### **ëª©ë¡ ìž‘ì„±** -```java -/** - *

주요 기능:

- *
    - *
  • 첫 번째 기능
  • - *
  • ë‘ ë²ˆì§¸ 기능
  • - *
  • 세 번째 기능
  • - *
- * - *

처리 과정:

- *
    - *
  1. 첫 번째 단계
  2. - *
  3. ë‘ ë²ˆì§¸ 단계
  4. - *
  5. 세 번째 단계
  6. - *
- */ -``` - -#### **코드 블ë¡** -```java -/** - *

사용 예시:

- *
- * AuthController controller = new AuthController();
- * LoginRequest request = new LoginRequest("user", "password");
- * ResponseEntity<LoginResponse> response = controller.login(request);
- * 
- */ -``` - -#### **í…Œì´ë¸”** -```java -/** - *

HTTP ìƒíƒœ 코드:

- * - * - * - * - * - *
ìƒíƒœ 코드설명
200성공
400ìž˜ëª»ëœ ìš”ì²­
401ì¸ì¦ 실패
- */ -``` - -### 3. **HTML 태그 사용 규칙** - -- **<와 >**: 제네릭 타입 표현 시 `<T>` 사용 -- **줄바꿈**: `
` 태그 사용 (ê°€ê¸‰ì  `

` 태그 권장) -- **ë§í¬**: `{@link ClassName#methodName}` 사용 -- **ì¸ë¼ì¸ 코드**: `{@code variableName}` ë˜ëŠ” `` 사용 - -## 📋 í´ëž˜ìФ ì£¼ì„ í‘œì¤€ - -### 1. **í´ëž˜ìФ ì£¼ì„ í…œí”Œë¦¿** -```java -/** - * í´ëž˜ìŠ¤ì˜ ê°„ë‹¨í•œ 설명 - * - *

í´ëž˜ìŠ¤ì˜ ìƒì„¸í•œ 설명과 목ì ì„ ì—¬ê¸°ì— ìž‘ì„±í•©ë‹ˆë‹¤.

- * - *

주요 기능:

- *
    - *
  • 기능 1
  • - *
  • 기능 2
  • - *
  • 기능 3
  • - *
- * - *

사용 예시:

- *
- * ClassName instance = new ClassName();
- * instance.someMethod();
- * 
- * - *

주ì˜ì‚¬í•­:

- *
    - *
  • 주ì˜ì‚¬í•­ 1
  • - *
  • 주ì˜ì‚¬í•­ 2
  • - *
- * - * @author 작성ìžëª… - * @version 1.0 - * @since 2024-01-01 - * - * @see 관련í´ëž˜ìФ1 - * @see 관련í´ëž˜ìФ2 - */ -public class ClassName { - // ... -} -``` - -### 2. **Controller í´ëž˜ìФ ì£¼ì„ ì˜ˆì‹œ** -```java -/** - * ì‚¬ìš©ìž ê´€ë¦¬ API 컨트롤러 - * - *

ì‚¬ìš©ìž ë“±ë¡, 조회, 수정, ì‚­ì œ ê¸°ëŠ¥ì„ ì œê³µí•˜ëŠ” REST API 컨트롤러입니다.

- * - *

주요 기능:

- *
    - *
  • ì‚¬ìš©ìž ë“±ë¡ ë° ì¸ì¦
  • - *
  • ì‚¬ìš©ìž ì •ë³´ 조회 ë° ìˆ˜ì •
  • - *
  • ì‚¬ìš©ìž ê¶Œí•œ 관리
  • - *
- * - *

API 엔드í¬ì¸íЏ:

- *
    - *
  • POST /api/users - ì‚¬ìš©ìž ë“±ë¡
  • - *
  • GET /api/users/{id} - ì‚¬ìš©ìž ì¡°íšŒ
  • - *
  • PUT /api/users/{id} - ì‚¬ìš©ìž ìˆ˜ì •
  • - *
  • DELETE /api/users/{id} - ì‚¬ìš©ìž ì‚­ì œ
  • - *
- * - *

보안 고려사항:

- *
    - *
  • 모든 엔드í¬ì¸íŠ¸ëŠ” ì¸ì¦ì´ 필요합니다
  • - *
  • ê°œì¸ì •ë³´ 처리 시 ë°ì´í„° 마스킹 ì ìš©
  • - *
  • 입력값 ê²€ì¦ ë° XSS ë°©ì§€
  • - *
- * - * @author cms-team - * @version 1.0 - * @since 2024-01-01 - * - * @see UserService - * @see UserRepository - * @see UserDTO - */ -@RestController -@RequestMapping("/api/users") -public class UserController { - // ... -} -``` - -## 📋 메서드 ì£¼ì„ í‘œì¤€ - -### 1. **메서드 ì£¼ì„ í…œí”Œë¦¿** -```java -/** - * ë©”ì„œë“œì˜ ê°„ë‹¨í•œ 설명 - * - *

ë©”ì„œë“œì˜ ìƒì„¸í•œ 설명과 ë™ìž‘ì„ ì—¬ê¸°ì— ìž‘ì„±í•©ë‹ˆë‹¤.

- * - *

처리 과정:

- *
    - *
  1. 첫 번째 단계
  2. - *
  3. ë‘ ë²ˆì§¸ 단계
  4. - *
  5. 세 번째 단계
  6. - *
- * - *

주ì˜ì‚¬í•­:

- *
    - *
  • 주ì˜ì‚¬í•­ 1
  • - *
  • 주ì˜ì‚¬í•­ 2
  • - *
- * - * @param param1 첫 번째 파ë¼ë¯¸í„° 설명 - * - 추가 ì„¤ëª…ì´ í•„ìš”í•œ 경우 - * @param param2 ë‘ ë²ˆì§¸ 파ë¼ë¯¸í„° 설명 - * - * @return 반환값 설명 - * - 성공 시: 설명 - * - 실패 시: 설명 - * - * @throws ExceptionType1 예외 ìƒí™© 1 설명 - * @throws ExceptionType2 예외 ìƒí™© 2 설명 - * - * @apiNote API 사용 시 주ì˜ì‚¬í•­ - * - * @see 관련메서드1 - * @see 관련메서드2 - * - * @since 1.0 - */ -public ReturnType methodName(Type param1, Type param2) { - // ... -} -``` - -### 2. **API 메서드 ì£¼ì„ ì˜ˆì‹œ** -```java -/** - * ì‚¬ìš©ìž ë¡œê·¸ì¸ ì²˜ë¦¬ - * - *

ì‚¬ìš©ìž ID와 비밀번호를 ê²€ì¦í•˜ì—¬ JWT 토í°ì„ ìƒì„±í•©ë‹ˆë‹¤.

- * - *

처리 과정:

- *
    - *
  1. 입력값 ê²€ì¦ (@Valid 어노테ì´ì…˜)
  2. - *
  3. ì‚¬ìš©ìž ì¸ì¦ ì •ë³´ 확ì¸
  4. - *
  5. JWT í† í° ìƒì„±
  6. - *
  7. ì‚¬ìš©ìž ì„¸ì…˜ 시작
  8. - *
  9. ë¡œê·¸ì¸ ë©”íŠ¸ë¦­ ì—…ë°ì´íЏ
  10. - *
- * - *

보안 고려사항:

- *
    - *
  • 비밀번호는 BCrypt로 ì•”í˜¸í™”ëœ ê°’ê³¼ 비êµ
  • - *
  • ë¡œê·¸ì¸ ì‹¤íŒ¨ 시 ìƒì„¸ ì •ë³´ 노출 ë°©ì§€
  • - *
  • ë¡œê·¸ì¸ ì‹œë„ ë¡œê·¸ 기ë¡
  • - *
- * - * @param request ë¡œê·¸ì¸ ìš”ì²­ ì •ë³´ - * - username: ì‚¬ìš©ìž ID (3-50ìž, 필수) - * - password: 비밀번호 (6-100ìž, 필수) - * - * @return ResponseEntity<LoginResponse> ë¡œê·¸ì¸ ì‘답 ì •ë³´ - * - 성공 시: 200 OK + JWT 토í°, ì‚¬ìš©ìž ì—­í• , 만료 시간 - * - 실패 시: 401 Unauthorized + ì—러 메시지 - * - * @throws InvalidCredentialsException ì¸ì¦ ì •ë³´ê°€ 올바르지 ì•Šì€ ê²½ìš° - * @throws RuntimeException ë¡œê·¸ì¸ ì²˜ë¦¬ 중 시스템 오류 ë°œìƒ ì‹œ - * - * @apiNote ë³´ì•ˆìƒ ì´ìœ ë¡œ ë¡œê·¸ì¸ ì‹¤íŒ¨ 시 구체ì ì¸ 실패 사유를 반환하지 않습니다. - * - * @see AuthService#login(LoginRequest) - * @see UserSessionService#startSession(String, String, java.time.Instant) - * - * @since 1.0 - */ -@PostMapping("/login") -public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - // ... -} -``` - -## 📋 필드 ì£¼ì„ í‘œì¤€ - -### 1. **필드 ì£¼ì„ í…œí”Œë¦¿** -```java -/** - * í•„ë“œì˜ ê°„ë‹¨í•œ 설명 - * - *

í•„ë“œì˜ ìƒì„¸í•œ 설명과 ìš©ë„를 ì—¬ê¸°ì— ìž‘ì„±í•©ë‹ˆë‹¤.

- * - *

주ì˜ì‚¬í•­:

- *
    - *
  • 주ì˜ì‚¬í•­ 1
  • - *
  • 주ì˜ì‚¬í•­ 2
  • - *
- * - * @since 1.0 - */ -private final ServiceType serviceName; -``` - -### 2. **ì˜ì¡´ì„± 주입 필드 예시** -```java -/** - * ì¸ì¦ 서비스 - * - *

ì‚¬ìš©ìž ë¡œê·¸ì¸/로그아웃 처리 ë° JWT í† í° ê´€ë¦¬ë¥¼ 담당합니다.

- * - *

주요 기능:

- *
    - *
  • ì‚¬ìš©ìž ì¸ì¦ ì •ë³´ ê²€ì¦
  • - *
  • JWT í† í° ìƒì„± ë° ê²€ì¦
  • - *
  • 로그ì¸/로그아웃 처리
  • - *
- * - * @see AuthService - * @since 1.0 - */ -private final AuthService authService; -``` - -## 📋 예외 í´ëž˜ìФ ì£¼ì„ í‘œì¤€ - -```java -/** - * ì‚¬ìš©ìž ì¸ì¦ 실패 예외 - * - *

ë¡œê·¸ì¸ ì‹œ ì‚¬ìš©ìž ID ë˜ëŠ” 비밀번호가 올바르지 ì•Šì„ ë•Œ ë°œìƒí•˜ëŠ” 예외입니다.

- * - *

ë°œìƒ ìƒí™©:

- *
    - *
  • 존재하지 않는 ì‚¬ìš©ìž ID
  • - *
  • ìž˜ëª»ëœ ë¹„ë°€ë²ˆí˜¸
  • - *
  • 계정 잠금 ìƒíƒœ
  • - *
- * - *

처리 방법:

- *
    - *
  • 사용ìžì—게 ì¼ë°˜ì ì¸ 오류 메시지 표시
  • - *
  • 보안 ë¡œê·¸ì— ìƒì„¸ ì •ë³´ 기ë¡
  • - *
  • 브루트 í¬ìФ 공격 ë°©ì§€ ë¡œì§ ì‹¤í–‰
  • - *
- * - * @author cms-team - * @version 1.0 - * @since 2024-01-01 - * - * @see AuthService - * @see SecurityException - */ -public class InvalidCredentialsException extends RuntimeException { - // ... -} -``` - -## 📋 ì¸í„°íŽ˜ì´ìФ ì£¼ì„ í‘œì¤€ - -```java -/** - * ì‚¬ìš©ìž ì¸ì¦ 서비스 ì¸í„°íŽ˜ì´ìФ - * - *

ì‚¬ìš©ìž ë¡œê·¸ì¸, 로그아웃, í† í° ê´€ë¦¬ 등 ì¸ì¦ 관련 ê¸°ëŠ¥ì„ ì •ì˜í•©ë‹ˆë‹¤.

- * - *

구현 í´ëž˜ìФ:

- *
    - *
  • {@link AuthServiceImpl} - 기본 구현체
  • - *
  • {@link LdapAuthService} - LDAP ì—°ë™ êµ¬í˜„ì²´
  • - *
- * - *

주요 기능:

- *
    - *
  • ì‚¬ìš©ìž ì¸ì¦ ë° í† í° ìƒì„±
  • - *
  • 로그아웃 ë° í† í° ë¬´íš¨í™”
  • - *
  • í† í° ìœ íš¨ì„± ê²€ì¦
  • - *
- * - * @author cms-team - * @version 1.0 - * @since 2024-01-01 - * - * @see AuthServiceImpl - * @see TokenProvider - */ -public interface AuthService { - // ... -} -``` - -## 📋 Enum ì£¼ì„ í‘œì¤€ - -```java -/** - * ì‚¬ìš©ìž ì—­í•  열거형 - * - *

시스템 사용ìžì˜ 권한 ìˆ˜ì¤€ì„ ì •ì˜í•©ë‹ˆë‹¤.

- * - *

권한 계층:

- *
    - *
  1. {@link #ADMIN} - 최고 ê´€ë¦¬ìž ê¶Œí•œ
  2. - *
  3. {@link #MANAGER} - ê´€ë¦¬ìž ê¶Œí•œ
  4. - *
  5. {@link #USER} - ì¼ë°˜ ì‚¬ìš©ìž ê¶Œí•œ
  6. - *
- * - * @author cms-team - * @version 1.0 - * @since 2024-01-01 - */ -public enum Role { - - /** - * 시스템 ê´€ë¦¬ìž - * - *

모든 시스템 ê¸°ëŠ¥ì— ëŒ€í•œ ì ‘ê·¼ ê¶Œí•œì„ ê°€ì§‘ë‹ˆë‹¤.

- * - *

주요 권한:

- *
    - *
  • ì‚¬ìš©ìž ê´€ë¦¬
  • - *
  • 시스템 설정
  • - *
  • 모든 ë°ì´í„° ì ‘ê·¼
  • - *
- */ - ADMIN, - - /** - * ê´€ë¦¬ìž - * - *

ì œí•œëœ ê´€ë¦¬ ê¸°ëŠ¥ì— ëŒ€í•œ ì ‘ê·¼ ê¶Œí•œì„ ê°€ì§‘ë‹ˆë‹¤.

- */ - MANAGER, - - /** - * ì¼ë°˜ ì‚¬ìš©ìž - * - *

기본ì ì¸ 시스템 ê¸°ëŠ¥ì— ëŒ€í•œ ì ‘ê·¼ ê¶Œí•œì„ ê°€ì§‘ë‹ˆë‹¤.

- */ - USER -} -``` - -## 📋 ì£¼ì„ ìž‘ì„± ì²´í¬ë¦¬ìŠ¤íŠ¸ - -### ✅ **í´ëž˜ìФ ì£¼ì„ ì²´í¬ë¦¬ìŠ¤íŠ¸** -- [ ] í´ëž˜ìŠ¤ì˜ ëª©ì ê³¼ ì—­í•  명시 -- [ ] 주요 기능 ëª©ë¡ ìž‘ì„± -- [ ] 사용 예시 코드 í¬í•¨ -- [ ] 주ì˜ì‚¬í•­ ë° ì œì•½ì‚¬í•­ 명시 -- [ ] @author, @version, @since 태그 작성 -- [ ] 관련 í´ëž˜ìФ @see 태그 추가 - -### ✅ **메서드 ì£¼ì„ ì²´í¬ë¦¬ìŠ¤íŠ¸** -- [ ] ë©”ì„œë“œì˜ ëª©ì ê³¼ ë™ìž‘ 설명 -- [ ] 처리 과정 단계별 설명 -- [ ] 모든 @param 태그 작성 -- [ ] @return 태그 작성 (void 메서드 제외) -- [ ] 가능한 예외 @throws 태그 작성 -- [ ] 보안 관련 주ì˜ì‚¬í•­ 명시 -- [ ] 관련 메서드 @see 태그 추가 - -### ✅ **HTML 태그 ì²´í¬ë¦¬ìŠ¤íŠ¸** -- [ ] 목ë¡ì€ `
    `, `
      `, `
    1. ` 태그 사용 -- [ ] 강조는 `` 태그 사용 -- [ ] ë‹¨ë½ êµ¬ë¶„ì€ `

      ` 태그 사용 -- [ ] 코드는 `` ë˜ëŠ” `

      ` 태그 사용
      -- [ ] 제네릭 íƒ€ìž…ì€ `<`, `>` 사용
      -
      -## 📋 ë„구 ë° ì„¤ì •
      -
      -### 1. **JavaDoc ìƒì„±**
      -```bash
      -# Gradle 프로ì íЏ
      -./gradlew javadoc
      -
      -# Maven 프로ì íЏ
      -mvn javadoc:javadoc
      -
      -# ì§ì ‘ 실행
      -javadoc -d docs -cp classpath src/**/*.java
      -```
      -
      -### 2. **IDE 설정**
      -- **IntelliJ IDEA**: Settings > Editor > Code Style > Java > JavaDoc
      -- **Eclipse**: Window > Preferences > Java > Code Style > Code Templates
      -- **VS Code**: Java Extension Pack + JavaDoc 플러그ì¸
      -
      -### 3. **ì •ì  ë¶„ì„ ë„구**
      -- **Checkstyle**: JavaDoc ëˆ„ë½ ê²€ì‚¬
      -- **SpotBugs**: ì£¼ì„ í’ˆì§ˆ 검사
      -- **SonarQube**: 문서화 품질 메트릭
      -
      -## 📋 참고 ìžë£Œ
      -
      -- [Oracle JavaDoc ê°€ì´ë“œ](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html)
      -- [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
      -- [Spring Framework ì£¼ì„ ìŠ¤íƒ€ì¼](https://github.com/spring-projects/spring-framework/wiki/Code-Style)
      -
      ----
      -
      -> **💡 íŒ**: ì´ ê°€ì´ë“œë¥¼ 팀 ë‚´ì—서 공유하고, 코드 리뷰 시 ì£¼ì„ í’ˆì§ˆë„ í•¨ê»˜ 검토하세요!
      \ No newline at end of file
      diff --git a/claude/standard_package_structure.md b/claude/standard_package_structure.md
      deleted file mode 100644
      index 81a4890..0000000
      --- a/claude/standard_package_structure.md
      +++ /dev/null
      @@ -1,173 +0,0 @@
      -패키지 구조 표준
      -
      -ë ˆì´ì–´ë“œ 아키í…처 패키지 구조
      -
      -├── {SERVICE}
      -│   ├── domain
      -│   ├── service
      -│   ├── controller
      -│   ├── dto
      -│   ├── repository
      -│   │   ├── jpa
      -│   │   └── entity
      -│   ├── config
      -└── common
      -        ├── dto
      -        ├── util
      -        ├── response
      -        └── exception
      -
      -Package명: 
      -- com.{ORG}.{ROOT}.{SERVICE}
      -예) com.unicorn.lifesub.mysub, com.unicorn.lifesub.common
      -
      -변수: 
      -- ORG: 회사 ë˜ëŠ” ì¡°ì§ëª…
      -- ROOT: Root Project 명
      -- SERVICE: 서비스명으로 Root Projectì˜ ì„œë¸Œ 프로ì íŠ¸ìž„
      -
      -
      -예시
      -
      -com.unicorn.lifesub.member
      - ├── MemberApplication.java
      - ├── controller
      - │   └── MemberController.java
      - ├── dto
      - │   ├── LoginRequest.java
      - │   ├── LogoutRequest.java
      - │   └── LogoutResponse.java  
      - ├── service
      - │   ├── MemberService.java
      - │   └── MemberServiceImpl.java
      - ├── domain
      - │   └── Member.java
      - ├── repository  
      - │   ├── entity
      - │   │   └── MemberEntity.java
      - │   └── jpa
      - │       └── MemberRepository.java
      - └── config
      -     ├── SecurityConfig.java
      -     ├── DataLoader.java
      -     ├── SwaggerConfig.java
      -     └── jwt
      -         ├── JwtAuthenticationFilter.java
      -         ├── JwtTokenProvider.java
      -         └── CustomUserDetailsService.java
      -
      -
      -í´ë¦° 아키í…처 패키지 구조 
      -
      -├── biz
      -│   ├── usecase
      -│   │   ├── in
      -│   │   ├── out
      -│   ├── service
      -│   └── domain
      -│   └── dto
      -├── infra
      -│   ├── controller
      -│   ├── dto
      -│   ├── gateway
      -│   │   ├── repository
      -│   │   └── entity
      -│   └── config    
      -
      -
      -Package명: 
      -- com.{ORG}.{ROOT}.{SERVICE}.biz
      -- com.{ORG}.{ROOT}.{SERVICE}.infra
      -예) com.unicorn.lifesub.mysub.biz, com.unicorn.lifesub.common
      -
      -변수: 
      -- ORG: 회사 ë˜ëŠ” ì¡°ì§ëª…
      -- ROOT: Root Project 명
      -- SERVICE: 서비스명으로 Root Projectì˜ ì„œë¸Œ 프로ì íŠ¸ìž„
      -
      -예시
      -
      -
      -com.unicorn.lifesub.mysub
      - ├── biz
      - │   ├── dto
      - │   │   ├── CategoryResponse.java
      - │   │   ├── ServiceListResponse.java
      - │   │   ├── MySubResponse.java
      - │   │   ├── SubDetailResponse.java
      - │   │   └── TotalFeeResponse.java
      - │   ├── service
      - │   │   ├── FeeLevel.java
      - │   │   └── MySubscriptionService.java
      - │   ├── usecase
      - │   │   ├── in
      - │   │   │   ├── CancelSubscriptionUseCase.java
      - │   │   │   ├── CategoryUseCase.java
      - │   │   │   ├── MySubscriptionsUseCase.java
      - │   │   │   ├── SubscribeUseCase.java
      - │   │   │   ├── SubscriptionDetailUseCase.java
      - │   │   │   └── TotalFeeUseCase.java
      - │   │   └── out 
      - │   │       ├── MySubscriptionReader.java
      - │   │       ├── MySubscriptionWriter.java
      - │   │       └── SubscriptionReader.java
      - │   └── domain
      - │       ├── Category.java
      - │       ├── MySubscription.java
      - │       └── Subscription.java
      - └── infra  
      -     ├── MySubApplication.java 
      -     ├── controller
      -     │   ├── CategoryController.java
      -     │   ├── MySubController.java
      -     │   └── ServiceController.java
      -     ├── config
      -     │   ├── DataLoader.java
      -     │   ├── SecurityConfig.java
      -     │   ├── SwaggerConfig.java
      -     │   └── jwt
      -     │       ├── JwtAuthenticationFilter.java
      -     │       └── JwtTokenProvider.java
      -     └── gateway
      -         ├── entity
      -         │   ├── CategoryEntity.java   
      -         │   ├── MySubscriptionEntity.java
      -         │   └── SubscriptionEntity.java
      -         ├── repository
      -         │   ├── CategoryJpaRepository.java
      -         │   ├── MySubscriptionJpaRepository.java
      -         │   └── SubscriptionJpaRepository.java  
      -         ├── MySubscriptionGateway.java
      -         └── SubscriptionGateway.java
      -
      -
      ----
      -
      -common 모듈 패키지 구조
      -
      -├── common
      -    ├── dto
      -    ├── entity
      -    ├── config
      -    ├── util
      -    └── exception
      -
      -
      -com.unicorn.lifesub.common
      - ├── dto
      - │   ├── ApiResponse.java
      - │   ├── JwtTokenDTO.java
      - │   ├── JwtTokenRefreshDTO.java
      - │   └── JwtTokenVerifyDTO.java
      - ├── config
      - │   └── JpaConfig.java
      - ├── entity
      - │   └── BaseTimeEntity.java        
      - ├── aop  
      - │   └── LoggingAspect.java
      - └── exception
      -     ├── ErrorCode.java
      -     ├── InfraException.java
      -     └── BusinessException.java
      -
      -
      diff --git a/claude/standard_testcode.md b/claude/standard_testcode.md
      deleted file mode 100644
      index 1eec6d5..0000000
      --- a/claude/standard_testcode.md
      +++ /dev/null
      @@ -1,214 +0,0 @@
      -1.TDD 기본 ì´í•´
      -
      -1) TDD ëª©ì   
      -   코드 품질 í–¥ìƒìœ¼ë¡œ 유지보수 비용 ì ˆê°
      -- 설계 품질 í–¥ìƒ: 테스트를 먼저 작성하면서 코드 구조와 ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 먼저 고민
      -- 회귀 버그 ë°©ì§€: 테스트 ìžë™í™”로 코드 변경 시 기존 ê¸°ëŠ¥ì˜ ì˜¤ìž‘ë™ì„ 빠르게 ê°ì§€
      -- ë¦¬íŒ©í† ë§ ê²€ì¦: 코드 개선 후 테스트 코드로 ê²€ì¦í•  수 있어 리팩토ë§ì— 대한 ìžì‹ ê° 확보
      -- 살아있는 문서: 테스트 ì½”ë“œì— ìƒ˜í”Œ ë°ì´í„°ë¥¼ ì´ìš©í•œ 예시가 있으므로 실제 ì½”ë“œì˜ ë™ìž‘ ë°©ì‹ì„ 문서화
      -
      ----  
      -
      -2) 테스트 유형
      -- 단위 테스트(Unit Test): 외부 기술요소(DB, 웹서버 등)ì™€ì˜ ì¸í„°íŽ˜ì´ìФ ì—†ì´ ë‹¨ìœ„ í´ëž˜ìŠ¤ì˜ í¼ë¸”릭 메소드 테스트
      -- 통합 테스트(Integration Test): ì¼ë¶€ 아키í…처 ì˜ì—­ì—서 외부 기술 요소와 ì¸í„°íŽ˜ì´ìŠ¤ê¹Œì§€ 테스트
      -- E2E 테스트(E2E Test): 모든 아키í…처 ì˜ì—­ì—서 외부 기술 요소와 ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 테스트
      -
      -* 아키í…처 ì˜ì—­: í´ëž˜ìŠ¤ë¥¼ 아키í…처ì ìœ¼ë¡œ 나눈 ë ˆì´ì–´ë¥¼ ì˜ë¯¸í•¨(예: controller, service, domain, repository)
      -
      ----
      -
      -3) 테스트 피ë¼ë¯¸ë“œ
      -
      -- 단위 테스트 70%, 통합 테스트 20%, E2E 테스트 10%ì˜ ë¹„ìœ¨ë¡œ 권장
      -- Mike Cohnì´ "Succeeding with Agile"ì—서 ì²˜ìŒ ì œì‹œí•œ ê°œë…
      -- 단위 테스트ì—서 E2E 테스트로 가면서 ì†ë„는 ëŠë ¤ì§€ê³  ë¹„ìš©ì€ ë†’ì•„ì§
      -
      ----
      -
      -4) Red-Green-Refactor 사ì´í´
      -
      -Red-Green-Refactor는 TDD(Test-Driven Development)를 수행하는 핵심 사ì´í´ìž„
      -- Red (실패하는 테스트 작성)
      -    - 새로운 ê¸°ëŠ¥ì— ëŒ€í•œ 테스트 코드를 먼저 작성
      -    - ì•„ì§ êµ¬í˜„ì´ ì—†ìœ¼ë¯€ë¡œ 테스트는 실패
      -    - ì´ ë‹¨ê³„ì—서 ê¸°ëŠ¥ì˜ ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 설계
      -- Green (테스트 통과하는 코드 작성)
      -    - 테스트를 통과하는 ìµœì†Œí•œì˜ ì½”ë“œ 작성
      -    - 품질보다는 ë™ìž‘ì— ì´ˆì 
      -- Refactor (리팩토ë§)
      -    - 중복 제거, ê°€ë…성 개선
      -    - 테스트는 ê³„ì† í†µê³¼í•˜ë„ë¡ ìœ ì§€
      -    - 코드 품질 개선
      -
      ----
      -2. 테스트 전략
      -
      -1) 테스트 수행 ì›ì¹™: FIRST ì›ì¹™
      -- Fast: 테스트는 빠르게 실행ë˜ì–´ì•¼ 함
      -- Isolated: ê° í…ŒìŠ¤íŠ¸ëŠ” ë…립ì ì´ì–´ì•¼ 함
      -- Repeatable: ì–´ë–¤ 환경ì—ì„œë„ ë™ì¼í•œ 결과가 나와야 함
      -- Self-validating: 테스트는 성공/실패가 명확해야 함
      -- Timely: 테스트는 실제 코드 작성 ì „/ì§í›„ì— ìž‘ì„±ë˜ì–´ì•¼ 함
      -
      ----
      -
      -2) 공통 전략: 테스트 코드 작성 관련
      -- 한 테스트는 한 가지만 테스트
      -- Given-When-Then 패턴 사용
      -    - Given(준비): í…ŒìŠ¤íŠ¸ì— í•„ìš”í•œ ìƒíƒœì™€ ë°ì´í„°ë¥¼ 설정
      -    - When(실행): 테스트하려는 ë™ìž‘ì„ ìˆ˜í–‰
      -    - Then(ê²€ì¦): 기대하는 결과가 나왔는지 확ì¸
      -- 깨ë—한 테스트 코드 작성
      -    - 테스트 ì˜ë„를 명확히 하는 네ì´ë°
      -    - 테스트 ì¼€ì´ìŠ¤ëŠ” 시나리오 중심으로 구성
      -    - 공통 ì„¤ì •ì€ ë³„ë„ ë©”ì„œë“œë¡œ 분리
      -    - 매ì§ë„˜ë²„ 대신 ìƒìˆ˜ 사용
      -    - 테스트 ë°ì´í„°ëŠ” 최소한으로 사용
      -- 경계값 테스트가 중요
      -    - null ê°’
      -    - 빈 컬렉션
      -    - 최대/최소값
      -    - 0ì´ë‚˜ 1ê³¼ ê°™ì€ íŠ¹ìˆ˜ê°’
      -    - ìž˜ëª»ëœ í¬ë§·ì˜ 입력값
      -
      ----
      -
      -2) 공통 전략: 테스트 코드 관리 관련
      -- 비용 효율ì ì¸ 테스트 ì „ëžµ
      -    - ìžì£¼ 변경ë˜ëŠ” 비즈니스 로ì§ì— 대한 테스트 ê°•í™”
      -    - 실제 ìš´ì˜ í™˜ê²½ê³¼ 유사한 통합 테스트 구성
      -    - 테스트 실행 시간과 리소스 사용량 모니터ë§
      -- ì§€ì†ì ì¸ 테스트 개선
      -    - 테스트 커버리지보다 테스트 품질 중시
      -    - 깨진 테스트는 즉시 수정하는 문화 정착
      -    - 테스트 ì½”ë“œë„ ì‹¤ì œ ì½”ë“œë§Œí¼ ì¤‘ìš”í•˜ê²Œ 관리
      -- 팀 í˜‘ì—…ì„ ìœ„í•œ ê°€ì´ë“œë¼ì¸ 수립
      -    - 테스트 네ì´ë° 컨벤션 수립
      -    - 테스트 ë°ì´í„° 관리 ì „ëžµ í•©ì˜
      -    - 테스트 실패 시 ëŒ€ì‘ í”„ë¡œì„¸ìŠ¤ 수립
      -
      ----
      -
      -3) 단위 테스트 전략
      -- 테스트 범위 명확화
      -    - í´ëž˜ìŠ¤ì˜ ê° public 메소드가 수행하는 ë‹¨ì¼ ì±…ìž„ì„ ê²€ì¦
      -    - private 메서드는 public 메서드를 통해 ê°„ì ‘ì ìœ¼ë¡œ 테스트
      -- 외부 ì˜ì¡´ì„± 처리
      -    - DB, 파ì¼, ë„¤íŠ¸ì›Œí¬ ë“± 외부 ì‹œìŠ¤í…œì€ ê°€ì§œ ê°ì²´ë¡œ 대체(Mocking)
      -    - 테스트 ë”블(ìŠ¤í„´íŠ¸ë§¨ì„ Stunt Doubleì´ë¼ê³  함. 대역으로 ì´í•´)ì€ ê¼­ 필요한 ë™ìž‘ë§Œ 구현
      -        - Mock: 메소드 호출 여부와 파ë¼ë¯¸í„° ê²€ì¦
      -        - Stub: ë°˜í™˜ê°’ì˜ ì¼ì¹˜ 여부 ê²€ì¦
      -        - Spy: Mocking하지 않고 실제 메소드를 ê°ì‹¸ì„œ 호출횟수, 호출순서등 추가 ì •ë³´ ê²€ì¦
      -- 격리성 확보
      -    - 테스트 ê°„ ìƒí˜¸ ì˜í–¥ ì—†ë„ë¡ ì„¤ê³„: ë™ì¼ 공유 ìžì›/ê°ì²´ë¥¼ 사용하지 않게 함
      -    - 테스트 실행 순서와 무관하게 ë™ìž‘
      -- ê°€ë…성과 유지보수성
      -    - 테스트 ëŒ€ìƒ í´ëž˜ìŠ¤ë‹¹ í•˜ë‚˜ì˜ í…ŒìŠ¤íŠ¸ í´ëž˜ìФ
      -    - 테스트 메서드는 한 가지 시나리오만 ê²€ì¦
      -
      ----
      -
      -4) 단위 테스트 시 Mocking 전략
      -- 외부 시스템(DB, 외부 API 등)ì€ ë°˜ë“œì‹œ Mocking
      -- ê°™ì€ ë ˆì´ì–´ì˜ ì˜ì¡´ì„± 있는 í´ëž˜ìŠ¤ëŠ” 실제 ê°ì²´ 사용
      -- 예외ì ìœ¼ë¡œ ì˜ì¡´ ê°ì²´ê°€ 매우 복잡하거나 무거운 경우 Mocking ê³ ë ¤
      -
      -* 참고: ëª¨ì˜ ê°ì²´ 테스트 ê· í˜•ì  ì°¾ê¸°  
      -  출처: When to mocking by Uncle Bob(https://blog.cleancoder.com/uncle-bob/2014/05/10/WhenToMock.html)
      -- ëª¨ì˜ ê°ì²´ë¥¼ ì´ìš© 안 하면: 테스트가 오래 걸리고 결과를 신뢰하기 어려우며 ì¸í”„ë¼ì— 너무 ë§Žì€ ì˜í–¥ì„ ë°›ìŒ
      -- ëª¨ì˜ ê°ì²´ë¥¼ 지나치게 사용하면: 복잡하고 ìˆ˜ì •ì— ì˜í–¥ì„ 너무 ë§Žì´ ë°›ìœ¼ë©° ëª¨ì˜ ì¸í„°íŽ˜ì´ìŠ¤ê°€ í­ë°œì ìœ¼ë¡œ ì¦ê°€
      -- ê· í˜•ì  ì°¾ê¸°
      -    - 아키í…처ì ìœ¼ë¡œ 중요한 경계ì—서만 ëª¨ì˜ í…ŒìŠ¤íŠ¸ë¥¼ 수행하고, ê·¸ 경계 안ì—서는 하지 않는다.  
      -      (Mock across architecturally significant boundaries, but not within those boundaries.)
      -    - 여기서 경계란 Controller, Service, Repository, Domainë“±ì˜ ë ˆì´ì–´ë¥¼ ì˜ë¯¸í•¨
      -
      ----
      -5) 통합 테스트 전략
      -- 웹 서버 ì¸í„°íŽ˜ì´ìФ
      -    - @WebMvcTest, @WebFluxTest 활용
      -    - Controller ê³„ì¸µì˜ ìš”ì²­/ì‘답 ê²€ì¦
      -    - Service ê³„ì¸µì€ Mocking 처리
      -
      -- Database ì¸í„°íŽ˜ì´ìФ
      -    - @DataJpaTest 활용
      -    - TestContainer로 실제 DB 엔진 실행
      -
      -- 외부 서비스 ì¸í„°íŽ˜ì´ìФ
      -    - WireMock ë“±ì„ í™œìš©í•œ Mocking
      -    - 실제 API 스펙 기반 테스트
      -
      -- 테스트 환경 구성
      -    - 테스트용 ë³„ë„ ì„¤ì • íŒŒì¼ êµ¬ì„±
      -    - 테스트 ë°ì´í„°ëŠ” 테스트 시작 시 초기화
      -    - @Transactionalì„ í™œìš©í•œ 테스트 격리
      -    - 테스트 ê°„ ë…립성 보장
      -
      ----
      -6) E2E 테스트 전략
      -- ì›ì¹™
      -    - 단위 테스트나 ì»´í¬ë„ŒíЏ 테스트ì—서 놓칠 수 있는 시나리오를 찾아내는 ê²ƒì´ ëª©í‘œìž„
      -    - 조건별 로ì§ì´ë‚˜ 분기 ìƒí™©(edge cases)ì´ ì•„ë‹Œ ìƒìœ„ ìˆ˜ì¤€ì˜ ì¼ë°˜ì ì¸ 시나리오만 테스트
      -    - 만약 ì–´ë–¤ 시스템 테스트 시나리오가 실패 í–ˆëŠ”ë° ë‹¨ìœ„ 테스트나 통합 테스트가 없다면 만들어야 함
      -
      -- ìš´ì˜ê³¼ ë™ì¼í•œ 테스트 환경 구성: 웹서버/WAS, DB, ìºì‹œ, MQ, 외부시스템
      -- 테스트 ë°ì´í„° 관리
      -    - 테스트용 마스터 ë°ì´í„° 구성
      -    - 시나리오별 테스트 ë°ì´í„° 세트 준비
      -    - ë°ì´í„° 초기화 ë° ì •ë¦¬ ìžë™í™”
      -- 테스트 ìžë™í™” ì „ëžµ
      -    - UI 테스트: Selenium, Cucumber, Playwright 등 ë„구 활용
      -    - API 테스트: Rest-Assured, Postman 등 ë„구 활용
      -
      ----
      -
      -7) 테스트 코드 네ì´ë° 컨벤션
      -
      -- 패키지 네ì´ë°
      -```
      -[Syntax]
      -{프로ë•션패키지}.test.{테스트유형}
      -
      -[Example]
      -- 단위테스트: com.company.order.test.unit
      -- 통합테스트: com.company.order.test.integration
      -- E2E테스트: com.company.order.test.e2e
      -```
      -
      -- í´ëž˜ìФ 네ì´ë°
      -```
      -[Syntax]
      -{대ìƒí´ëž˜ìФ}{테스트유형}Test
      -
      -[Example]
      -- 단위테스트: OrderServiceUnitTest
      -- 통합테스트: OrderServiceIntegrationTest
      -- E2E테스트: OrderServiceE2ETest
      -```
      -
      -- 메소드 네ì´ë°
      -```
      -[Syntax]
      -given{초기ìƒíƒœ}_when{행위}_then{ê²°ê³¼}
      -
      -[Example]
      -givenEmptyCart_whenAddItem_thenSuccess()
      -givenInvalidToken_whenAuthenticate_thenThrowException()
      -givenExistingUser_whenUpdateProfile_thenProfileUpdated()
      -```
      -
      -- 테스트 ë°ì´í„° 네ì´ë°
      -```
      -[Syntax]
      -ìƒìˆ˜: {ìƒíƒœ}_{대ìƒ}
      -변수: {ìƒíƒœ}{대ìƒ}
      -
      -[Example]
      -// ìƒìˆ˜
      -VALID_USER_ID = 1L
      -EMPTY_ORDER_LIST = Collections.emptyList()
      -
      -// 변수
      -normalUser = new User(...)
      -emptyCart = new Cart()
      -```
      diff --git a/common/build.gradle b/common/build.gradle
      index f1d6d37..47154f5 100644
      --- a/common/build.gradle
      +++ b/common/build.gradle
      @@ -32,4 +32,7 @@ dependencies {
           // Jackson for JSON
           api 'com.fasterxml.jackson.core:jackson-databind'
           api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
      +
      +    // Swagger/OpenAPI
      +    api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
       }
      
      From c152faff549b88c6da211a0ee8b89b13e227f87d Mon Sep 17 00:00:00 2001
      From: cherry2250 
      Date: Tue, 28 Oct 2025 09:40:53 +0900
      Subject: [PATCH 13/61] =?UTF-8?q?Claude=20=ED=8F=B4=EB=8D=94=20=EC=9B=90?=
       =?UTF-8?q?=EB=B3=B5?=
      MIME-Version: 1.0
      Content-Type: text/plain; charset=UTF-8
      Content-Transfer-Encoding: 8bit
      
      ---
       claude/api-design.md                  | 111 ++++
       claude/architecture-patterns-guide.md | 169 ++++++
       claude/architecture-patterns.md       | 169 ++++++
       claude/cloud-design-patterns.md       | 104 ++++
       claude/common-principles.md           | 197 ++++++
       claude/conversation-summary.md        | 823 ++++++++++++++++++++++++++
       claude/logical-architecture-design.md |  64 ++
       claude/mermaid-guide.md               | 300 ++++++++++
       claude/plantuml-guide.md              |  82 +++
       claude/sequence-inner-design.md       |  76 +++
       claude/sequence-outer-design.md       |  54 ++
       claude/standard_comment.md            | 518 ++++++++++++++++
       claude/standard_package_structure.md  | 173 ++++++
       claude/standard_testcode.md           | 214 +++++++
       14 files changed, 3054 insertions(+)
       create mode 100644 claude/api-design.md
       create mode 100644 claude/architecture-patterns-guide.md
       create mode 100644 claude/architecture-patterns.md
       create mode 100644 claude/cloud-design-patterns.md
       create mode 100644 claude/common-principles.md
       create mode 100644 claude/conversation-summary.md
       create mode 100644 claude/logical-architecture-design.md
       create mode 100644 claude/mermaid-guide.md
       create mode 100644 claude/plantuml-guide.md
       create mode 100644 claude/sequence-inner-design.md
       create mode 100644 claude/sequence-outer-design.md
       create mode 100644 claude/standard_comment.md
       create mode 100644 claude/standard_package_structure.md
       create mode 100644 claude/standard_testcode.md
      
      diff --git a/claude/api-design.md b/claude/api-design.md
      new file mode 100644
      index 0000000..d44c64b
      --- /dev/null
      +++ b/claude/api-design.md
      @@ -0,0 +1,111 @@
      +# API설계가ì´ë“œ
      +
      +[요청사항]  
      +- <작성ì›ì¹™>ì„ ì¤€ìš©í•˜ì—¬ 설계
      +- <작성순서>ì— ë”°ë¼ ì„¤ê³„
      +- [결과파ì¼] ì•ˆë‚´ì— ë”°ë¼ íŒŒì¼ ìž‘ì„± 
      +- 최종 완료 후 API í™•ì¸ ë°©ë²• 안내 
      +  - https://editor.swagger.io/ ì ‘ê·¼  
      +  - ìƒì„±ëœ swagger yaml파ì¼ì„ 붙여서 í™•ì¸ ë° í…ŒìŠ¤íŠ¸  
      +
      +[ê°€ì´ë“œ]  
      +<작성 ì›ì¹™>
      +- ê° ì„œë¹„ìŠ¤ API는 ë…립ì ìœ¼ë¡œ 완전한 명세를 í¬í•¨
      +- 공통 스키마는 ê° ì„œë¹„ìŠ¤ì—서 í•„ìš”ì— ë”°ë¼ ì§ì ‘ ì •ì˜
      +- 서비스 ê°„ ì˜ì¡´ì„±ì„ 최소화하여 ë…립 ë°°í¬ ê°€ëŠ¥
      +- 중복ë˜ëŠ” 스키마가 많아질 경우ì—ë§Œ 공통 íŒŒì¼ ë„ìž… 검토
      +<작성순서>
      +- 준비: 
      +  - 유저스토리, 외부시퀀스설계서, 내부시퀀스설계서 ë¶„ì„ ë° ì´í•´ 
      +- 실행:
      +  - <병렬처리> ì•ˆë‚´ì— ë”°ë¼ ë™ì‹œ 수행
      +  - ì— ë”°ë¼ API ì„ ì • 
      +  - <파ì¼ìž‘성안내>ì— ë”°ë¼ ìž‘ì„±  
      +  - <ê²€ì¦ë°©ë²•>ì— ë”°ë¼ ìž‘ì„±ëœ YAMLì˜ ë¬¸ë²• ë° êµ¬ì¡° ê²€ì¦ ìˆ˜í–‰
      +- 검토:
      +  - <작성ì›ì¹™> 준수 검토
      +  - 스쿼드 íŒ€ì› ë¦¬ë·°: ëˆ„ë½ ë° ê°œì„  사항 검토
      +  - 수정 사항 ì„ íƒ ë° ë°˜ì˜ 
      +
      +
      +- 유저스토리와 매칭 ë˜ì–´ì•¼ 함. 불필요한 추가 설계 금지  
      +  (유저스토리 ID를 x-user-story 확장 í•„ë“œì— ëª…ì‹œ)
      +- '외부시퀀스설계서'/'내부시퀀스설계서'와 ì¼ê´€ì„± 있게 ì„ ì • 
      +
      +<파ì¼ìž‘성안내>
      +- OpenAPI 3.0 스펙 준용 
      +- **servers 섹션 필수화**
      +  - 모든 OpenAPI ëª…ì„¸ì— servers 섹션 í¬í•¨
      +  - SwaggerHub Mock URLì„ ì²« 번째 옵션으로 배치
      +- **example ë°ì´í„° 권장**
      +  - ìŠ¤í‚¤ë§ˆì— exampleì„ ì¶”ê°€í•˜ì—¬ Swagger UIì—서 테스트 í•  수 있게함 
      +- **테스트 시나리오 í¬í•¨**
      +  - ê° API 엔드í¬ì¸íŠ¸ë³„ 테스트 ì¼€ì´ìФ ì •ì˜
      +  - 성공/실패 ì¼€ì´ìФ ëª¨ë‘ í¬í•¨
      +- 작성 형ì‹
      +  - YAML 형ì‹ì˜ OpenAPI 3.0 명세
      +  - ê° API별 필수 항목:
      +    - summary: API ëª©ì  ì„¤ëª…
      +    - operationId: 고유 ì‹ë³„ìž
      +    - x-user-story: 유저스토리 ID
      +    - x-controller: 담당 컨트롤러
      +    - tags: API 그룹 분류
      +    - requestBody/responses: ìƒì„¸ 스키마
      +  - ê° ì„œë¹„ìŠ¤ 파ì¼ì— 필요한 모든 스키마 í¬í•¨:
      +    - components/schemas: 요청/ì‘답 모ë¸
      +    - components/parameters: 공통 파ë¼ë¯¸í„°
      +    - components/responses: 공통 ì‘답
      +    - components/securitySchemes: ì¸ì¦ ë°©ì‹
      +
      +<íŒŒì¼ êµ¬ì¡°>
      +```
      +design/backend/api/
      +├── {service-name}-api.yaml      # ê° ë§ˆì´í¬ë¡œì„œë¹„스별 API 명세
      +└── ...                          # 추가 서비스들
      +
      +예시:
      +├── profile-service-api.yaml     # í”„ë¡œíŒŒì¼ ì„œë¹„ìŠ¤ API
      +├── order-service-api.yaml       # 주문 서비스 API
      +└── payment-service-api.yaml     # 결제 서비스 API
      +```
      +
      +- 파ì¼ëª… 규칙
      +  - ì„œë¹„ìŠ¤ëª…ì€ kebab-case로 작성
      +  - 파ì¼ëª… 형ì‹: {service-name}-api.yaml
      +  - ì„œë¹„ìŠ¤ëª…ì€ ìœ ì €ìŠ¤í† ë¦¬ì˜ '서비스' í•­ëª©ì„ ì˜ë¬¸ìœ¼ë¡œ 변환하여 사용
      +
      +<병렬처리>
      +- **ì˜ì¡´ì„± ë¶„ì„ ì„ í–‰**: 병렬 처리 ì „ 반드시 ì˜ì¡´ì„± 파악
      +- **순차 처리 필요시**: 무리한 병렬화보다는 안전한 순차 처리
      +- **ê²€ì¦ ë‹¨ê³„ 필수**: 병렬 처리 후 통합 ê²€ì¦
      +
      +<ê²€ì¦ë°©ë²•>
      +- swagger-cli를 사용한 ìžë™ ê²€ì¦ ìˆ˜í–‰
      +- ê²€ì¦ ëª…ë ¹ì–´: `swagger-cli validate {파ì¼ëª…}`
      +- swagger-cliê°€ ì—†ì„ ê²½ìš° ìžë™ 설치:
      +  ```bash
      +  # swagger-cli 설치 í™•ì¸ ë° ìžë™ 설치
      +  command -v swagger-cli >/dev/null 2>&1 || npm install -g @apidevtools/swagger-cli
      +  
      +  # ê²€ì¦ ì‹¤í–‰
      +  swagger-cli validate design/backend/api/*.yaml
      +  ```
      +- ê²€ì¦ í•­ëª©:
      +  - OpenAPI 3.0 스펙 준수
      +  - YAML 구문 오류
      +  - 스키마 참조 유효성
      +  - 필수 필드 존재 여부
      +
      +[참고ìžë£Œ]
      +- 유저스토리
      +- 외부시퀀스설계서
      +- 내부시퀀스설계서
      +- OpenAPI 스펙: https://swagger.io/specification/
      +
      +[예시]
      +- swagger api yaml: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/samples/sample-swagger-api.yaml
      +- API설계서: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/samples/sample-API%20설계서.md
      +
      +[결과파ì¼]
      +- ê° ì„œë¹„ìŠ¤ë³„ë¡œ 별ë„ì˜ YAML íŒŒì¼ ìƒì„± 
      +- design/backend/api/*.yaml (OpenAPI 형ì‹)
      \ No newline at end of file
      diff --git a/claude/architecture-patterns-guide.md b/claude/architecture-patterns-guide.md
      new file mode 100644
      index 0000000..4177e80
      --- /dev/null
      +++ b/claude/architecture-patterns-guide.md
      @@ -0,0 +1,169 @@
      +# í´ë¼ìš°ë“œ 아키í…처패턴선정 ê°€ì´ë“œ
      +
      +## 개요
      +ì´ ê°€ì´ë“œëŠ” 마ì´í¬ë¡œì„œë¹„스 기반 í´ë¼ìš°ë“œ ì‹œìŠ¤í…œì„ ìœ„í•œ 아키í…처 패턴 ì„ ì • ë°©ë²•ë¡ ì„ ì œê³µí•©ë‹ˆë‹¤. 체계ì ì¸ ë¶„ì„ê³¼ ì •ëŸ‰ì  í‰ê°€ë¥¼ 통해 최ì ì˜ íŒ¨í„´ì„ ì„ ì •í•  수 있습니다.
      +
      +## 1. 요구사항 ë¶„ì„
      +
      +### 1.1 유저스토리 ë¶„ì„
      +ê° ì„œë¹„ìŠ¤ë³„ë¡œ 기능ì /ë¹„ê¸°ëŠ¥ì  ìš”êµ¬ì‚¬í•­ì„ ëª…í™•ížˆ ë„출합니다.
      +
      +**ê¸°ëŠ¥ì  ìš”êµ¬ì‚¬í•­**:
      +- ê° ìœ ì €ìŠ¤í† ë¦¬ì—서 요구하는 핵심 기능
      +- 서비스 ê°„ ë°ì´í„° êµí™˜ 요구사항
      +- 비즈니스 로ì§ì˜ 복잡ë„와 특성
      +
      +**ë¹„ê¸°ëŠ¥ì  ìš”êµ¬ì‚¬í•­**:
      +- 성능 요구사항 (ì‘답시간, 처리량)
      +- 가용성 ë° ì‹ ë¢°ì„± 요구사항
      +- 확장성 ë° ìœ ì§€ë³´ìˆ˜ì„± 요구사항
      +- 보안 ë° ì»´í”Œë¼ì´ì–¸ìФ 요구사항
      +
      +### 1.2 UI/UX설계 ë¶„ì„
      +Wireframeì„ í†µí•´ ì‚¬ìš©ìž ì¸í„°ëž™ì…˜ 패턴과 ë°ì´í„° 플로우를 파악합니다.
      +
      +**ë¶„ì„ í•­ëª©**:
      +- ì‚¬ìš©ìž ì¸í„°ëž™ì…˜ 패턴 (ë™ê¸°/비ë™ê¸° 처리 필요성)
      +- ë°ì´í„° 조회/변경 패턴
      +- 화면 ê°„ 전환 í름
      +- 실시간 ì—…ë°ì´íЏ 요구사항
      +
      +### 1.3 통합 ë¶„ì„
      +유저스토리와 UI/UX 설계를 연계하여 **ê¸°ìˆ ì  ë„전과제를 ì‹ë³„**합니다.
      +
      +**ë„전과제 ì‹ë³„**:
      +- 복잡한 비즈니스 트랜잭션
      +- 대용량 ë°ì´í„° 처리
      +- 실시간 처리 요구사항
      +- 외부 시스템 ì—°ë™ ë³µìž¡ì„±
      +- 서비스 ê°„ ì˜ì¡´ì„± 관리
      +
      +## 2. 패턴 선정
      +
      +### 2.1 í‰ê°€ 기준
      +ë‹¤ìŒ 5가지 기준으로 ê° íŒ¨í„´ì„ ì •ëŸ‰ì ìœ¼ë¡œ í‰ê°€í•©ë‹ˆë‹¤.
      +
      +| 기준 | 가중치 | í‰ê°€ ë‚´ìš© |
      +|------|--------|-----------|
      +| **기능 ì í•©ì„±** | 35% | ìš”êµ¬ì‚¬í•­ì„ ì§ì ‘ 해결하는 능력 |
      +| **성능 효과** | 25% | ì‘답시간 ë° ì²˜ë¦¬ëŸ‰ 개선 효과 |
      +| **ìš´ì˜ ë³µìž¡ë„** | 20% | 구현 ë° ìš´ì˜ì˜ ìš©ì´ì„± |
      +| **확장성** | 15% | 미래 ìš”êµ¬ì‚¬í•­ì— ëŒ€í•œ 대ì‘ë ¥ |
      +| **비용 효율성** | 5% | 개발/ìš´ì˜ ë¹„ìš© 대비 효과(ROI) |
      +
      +### 2.2 ì •ëŸ‰ì  í‰ê°€ 방법
      +
      +**í‰ê°€ ì²™ë„**: 1-10ì  (10ì ì´ 가장 우수)
      +
      +**패턴별 í‰ê°€ 매트릭스 예시**:
      +
      +| 패턴 | 기능 ì í•©ì„±
      (35%) | 성능 효과
      (25%) | ìš´ì˜ ë³µìž¡ë„
      (20%) | 확장성
      (15%) | 비용 효율성
      (5%) | **ì´ì ** | +|------|:---:|:---:|:---:|:---:|:---:|:---:| +| API Gateway | 8 × 0.35 = 2.8 | 7 × 0.25 = 1.75 | 8 × 0.20 = 1.6 | 9 × 0.15 = 1.35 | 7 × 0.05 = 0.35 | **7.85** | +| CQRS | 9 × 0.35 = 3.15 | 9 × 0.25 = 2.25 | 5 × 0.20 = 1.0 | 8 × 0.15 = 1.2 | 6 × 0.05 = 0.3 | **7.90** | +| Event Sourcing | 7 × 0.35 = 2.45 | 8 × 0.25 = 2.0 | 4 × 0.20 = 0.8 | 9 × 0.15 = 1.35 | 5 × 0.05 = 0.25 | **6.85** | + +### 2.3 단계별 ì ìš© 로드맵 +MVP → 확장 → ê³ ë„í™” 3단계로 구분하여 ì ì§„ì  ì ìš© 계íšì„ 수립합니다. + +**Phase 1: MVP (Minimum Viable Product)** +- 핵심 비즈니스 기능 중심 +- 단순하고 안정ì ì¸ 패턴 ìš°ì„  +- 빠른 출시를 위한 최소 기능 + +**Phase 2: 확장 (Scale-up)** +- ì‚¬ìš©ìž ì¦ê°€ì— 따른 성능 최ì í™” +- 고급 패턴 ë„ìž… +- ëª¨ë‹ˆí„°ë§ ë° ìš´ì˜ ìžë™í™” + +**Phase 3: ê³ ë„í™” (Advanced)** +- 복잡한 비즈니스 요구사항 ëŒ€ì‘ +- 최신 기술 ë° íŒ¨í„´ ì ìš© +- 글로벌 확장 대비 + +## 3. 문서 작성 + +### 3.1 êµ¬ì¡°í™”ëœ ìž‘ì„± 순서 +1. **요구사항 ë¶„ì„ ê²°ê³¼** +2. **패턴 í‰ê°€** (í‰ê°€ 매트릭스 í¬í•¨) +3. **ì ìš© 설계** (Mermaid 다ì´ì–´ê·¸ëž¨) +4. **구현 계íš** (Phase별 로드맵) + +### 3.2 Mermaid 다ì´ì–´ê·¸ëž¨ 작성 +서비스 아키í…처와 패턴 ì ìš©ì„ 시ê°ì ìœ¼ë¡œ 표현합니다. + +```mermaid +graph TB + Client[í´ë¼ì´ì–¸íЏ] --> Gateway[API Gateway] + Gateway --> Auth[ì¸ì¦ 서비스] + Gateway --> UserSvc[ì‚¬ìš©ìž ì„œë¹„ìŠ¤] + Gateway --> OrderSvc[주문 서비스] + + OrderSvc --> EventBus[ì´ë²¤íЏ 버스] + EventBus --> PaymentSvc[ê²°ì œ 서비스] + EventBus --> NotificationSvc[알림 서비스] + + UserSvc --> UserDB[(ì‚¬ìš©ìž DB)] + OrderSvc --> OrderDB[(주문 DB)] + PaymentSvc --> PaymentDB[(ê²°ì œ DB)] +``` + +### 3.3 ì‹¤ìš©ì  ë‚´ìš© í¬í•¨ +- **코드 예시**: 패턴 êµ¬í˜„ì„ ìœ„í•œ 구체ì ì¸ 코드 스니펫 +- **구현 시 고려사항**: 실제 개발 시 주ì˜í•  ì  +- **ì˜ˆìƒ íš¨ê³¼**: ì •ëŸ‰ì  ì„±ê³¼ 지표 (ì‘답시간 개선, 처리량 ì¦ê°€ 등) + +## 참고 ìžë£Œ +- **유저스토리** +- UI/UX설계서 +- **í´ë¼ìš°ë“œì•„키í…처패턴요약표** + +## ê²°ê³¼ íŒŒì¼ +ì„ ì •ëœ ì•„í‚¤í…처 íŒ¨í„´ì€ ë‹¤ìŒê³¼ ê°™ì´ ë¬¸ì„œí™”ë©ë‹ˆë‹¤: + +### 파ì¼ëª… +design/pattern/architecture-pattern.md + +### 필수 í¬í•¨ ë‚´ìš© +1. **요구사항 ë¶„ì„ ê²°ê³¼** + - 기능ì /ë¹„ê¸°ëŠ¥ì  ìš”êµ¬ì‚¬í•­ ìƒì„¸ ë¶„ì„ + - ê¸°ìˆ ì  ë„전과제 ì‹ë³„ + +2. **패턴 ì„ ì • 매트릭스 (í‰ê°€í‘œ)** + - 후보 패턴별 ì •ëŸ‰ì  í‰ê°€ ì ìˆ˜ + - ì„ ì • 근거 ë° ì´ìœ  + +3. **서비스별 패턴 ì ìš© 설계 (Mermaid)** + - ì „ì²´ 아키í…처 구조 + - 패턴별 ì ìš© ì˜ì—­ 표시 + +4. **Phase별 구현 로드맵** + - 단계별 ì ìš© ê³„íš + - 마ì¼ìŠ¤í†¤ ë° ëª©í‘œ 설정 + +5. **ì˜ˆìƒ ì„±ê³¼ 지표** + - 성능 개선 예ìƒì¹˜ + - 비용 ì ˆê° íš¨ê³¼ + - 개발 ìƒì‚°ì„± í–¥ìƒ + +## ì²´í¬ë¦¬ìŠ¤íŠ¸ + +작성 완료 후 ë‹¤ìŒ í•­ëª©ë“¤ì„ ê²€í† í•˜ì„¸ìš”: + +- [ ] **ê° ìœ ì €ìŠ¤í† ë¦¬ê°€ ì–´ë–¤ 패턴으로 í•´ê²°ë˜ëŠ”ì§€ 명시했는가?** +- [ ] **패턴 ì„ ì • ì´ìœ ë¥¼ 정량ì ìœ¼ë¡œ 설명했는가?** +- [ ] **패턴 ê°„ ìƒí˜¸ìž‘용과 통합 아키í…처를 표현했는가?** +- [ ] **구현 우선순위와 단계별 목표가 명확한가?** +- [ ] **실무ìžê°€ 바로 활용할 수 있는 수준ì¸ê°€?** + +## 작성 시 주ì˜ì‚¬í•­ + +1. **ê°ê´€ì  í‰ê°€**: ì£¼ê´€ì  íŒë‹¨ë³´ë‹¤ëŠ” ì •ëŸ‰ì  ë°ì´í„° 기반 ì„ ì • +2. **현실성**: íŒ€ì˜ ê¸°ìˆ  수준과 프로ì íЏ ì¼ì •ì„ ê³ ë ¤í•œ 실현 가능한 패턴 ì„ ì • +3. **확장성**: 현재 요구사항ë¿ë§Œ ì•„ë‹ˆë¼ ë¯¸ëž˜ 확장성까지 ê³ ë ¤ +4. **비용 효율성**: ê³¼ë„한 ì—”ì§€ë‹ˆì–´ë§ ì§€ì–‘, 비즈니스 가치 중심 ì„ ì • +5. **문서화**: ì„ ì • 과정과 근거를 명확히 문서화하여 í›„ì† ì˜ì‚¬ê²°ì • ì§€ì› + +## 완료 후 mermaid 스í¬ë¦½íЏ 테스트 방법 안내 +- https://mermaid.live/edit ì— ì ‘ê·¼ +- 스í¬ë¦½íЏ ë‚´ìš©ì„ ë¶™ì—¬ë„£ì–´ í™•ì¸ \ No newline at end of file diff --git a/claude/architecture-patterns.md b/claude/architecture-patterns.md new file mode 100644 index 0000000..4177e80 --- /dev/null +++ b/claude/architecture-patterns.md @@ -0,0 +1,169 @@ +# í´ë¼ìš°ë“œ 아키í…처패턴선정 ê°€ì´ë“œ + +## 개요 +ì´ ê°€ì´ë“œëŠ” 마ì´í¬ë¡œì„œë¹„스 기반 í´ë¼ìš°ë“œ ì‹œìŠ¤í…œì„ ìœ„í•œ 아키í…처 패턴 ì„ ì • ë°©ë²•ë¡ ì„ ì œê³µí•©ë‹ˆë‹¤. 체계ì ì¸ ë¶„ì„ê³¼ ì •ëŸ‰ì  í‰ê°€ë¥¼ 통해 최ì ì˜ íŒ¨í„´ì„ ì„ ì •í•  수 있습니다. + +## 1. 요구사항 ë¶„ì„ + +### 1.1 유저스토리 ë¶„ì„ +ê° ì„œë¹„ìŠ¤ë³„ë¡œ 기능ì /ë¹„ê¸°ëŠ¥ì  ìš”êµ¬ì‚¬í•­ì„ ëª…í™•ížˆ ë„출합니다. + +**ê¸°ëŠ¥ì  ìš”êµ¬ì‚¬í•­**: +- ê° ìœ ì €ìŠ¤í† ë¦¬ì—서 요구하는 핵심 기능 +- 서비스 ê°„ ë°ì´í„° êµí™˜ 요구사항 +- 비즈니스 로ì§ì˜ 복잡ë„와 특성 + +**ë¹„ê¸°ëŠ¥ì  ìš”êµ¬ì‚¬í•­**: +- 성능 요구사항 (ì‘답시간, 처리량) +- 가용성 ë° ì‹ ë¢°ì„± 요구사항 +- 확장성 ë° ìœ ì§€ë³´ìˆ˜ì„± 요구사항 +- 보안 ë° ì»´í”Œë¼ì´ì–¸ìФ 요구사항 + +### 1.2 UI/UX설계 ë¶„ì„ +Wireframeì„ í†µí•´ ì‚¬ìš©ìž ì¸í„°ëž™ì…˜ 패턴과 ë°ì´í„° 플로우를 파악합니다. + +**ë¶„ì„ í•­ëª©**: +- ì‚¬ìš©ìž ì¸í„°ëž™ì…˜ 패턴 (ë™ê¸°/비ë™ê¸° 처리 필요성) +- ë°ì´í„° 조회/변경 패턴 +- 화면 ê°„ 전환 í름 +- 실시간 ì—…ë°ì´íЏ 요구사항 + +### 1.3 통합 ë¶„ì„ +유저스토리와 UI/UX 설계를 연계하여 **ê¸°ìˆ ì  ë„전과제를 ì‹ë³„**합니다. + +**ë„전과제 ì‹ë³„**: +- 복잡한 비즈니스 트랜잭션 +- 대용량 ë°ì´í„° 처리 +- 실시간 처리 요구사항 +- 외부 시스템 ì—°ë™ ë³µìž¡ì„± +- 서비스 ê°„ ì˜ì¡´ì„± 관리 + +## 2. 패턴 ì„ ì • + +### 2.1 í‰ê°€ 기준 +ë‹¤ìŒ 5가지 기준으로 ê° íŒ¨í„´ì„ ì •ëŸ‰ì ìœ¼ë¡œ í‰ê°€í•©ë‹ˆë‹¤. + +| 기준 | 가중치 | í‰ê°€ ë‚´ìš© | +|------|--------|-----------| +| **기능 ì í•©ì„±** | 35% | ìš”êµ¬ì‚¬í•­ì„ ì§ì ‘ 해결하는 능력 | +| **성능 효과** | 25% | ì‘답시간 ë° ì²˜ë¦¬ëŸ‰ 개선 효과 | +| **ìš´ì˜ ë³µìž¡ë„** | 20% | 구현 ë° ìš´ì˜ì˜ ìš©ì´ì„± | +| **확장성** | 15% | 미래 ìš”êµ¬ì‚¬í•­ì— ëŒ€í•œ 대ì‘ë ¥ | +| **비용 효율성** | 5% | 개발/ìš´ì˜ ë¹„ìš© 대비 효과(ROI) | + +### 2.2 ì •ëŸ‰ì  í‰ê°€ 방법 + +**í‰ê°€ ì²™ë„**: 1-10ì  (10ì ì´ 가장 우수) + +**패턴별 í‰ê°€ 매트릭스 예시**: + +| 패턴 | 기능 ì í•©ì„±
      (35%) | 성능 효과
      (25%) | ìš´ì˜ ë³µìž¡ë„
      (20%) | 확장성
      (15%) | 비용 효율성
      (5%) | **ì´ì ** | +|------|:---:|:---:|:---:|:---:|:---:|:---:| +| API Gateway | 8 × 0.35 = 2.8 | 7 × 0.25 = 1.75 | 8 × 0.20 = 1.6 | 9 × 0.15 = 1.35 | 7 × 0.05 = 0.35 | **7.85** | +| CQRS | 9 × 0.35 = 3.15 | 9 × 0.25 = 2.25 | 5 × 0.20 = 1.0 | 8 × 0.15 = 1.2 | 6 × 0.05 = 0.3 | **7.90** | +| Event Sourcing | 7 × 0.35 = 2.45 | 8 × 0.25 = 2.0 | 4 × 0.20 = 0.8 | 9 × 0.15 = 1.35 | 5 × 0.05 = 0.25 | **6.85** | + +### 2.3 단계별 ì ìš© 로드맵 +MVP → 확장 → ê³ ë„í™” 3단계로 구분하여 ì ì§„ì  ì ìš© 계íšì„ 수립합니다. + +**Phase 1: MVP (Minimum Viable Product)** +- 핵심 비즈니스 기능 중심 +- 단순하고 안정ì ì¸ 패턴 ìš°ì„  +- 빠른 출시를 위한 최소 기능 + +**Phase 2: 확장 (Scale-up)** +- ì‚¬ìš©ìž ì¦ê°€ì— 따른 성능 최ì í™” +- 고급 패턴 ë„ìž… +- ëª¨ë‹ˆí„°ë§ ë° ìš´ì˜ ìžë™í™” + +**Phase 3: ê³ ë„í™” (Advanced)** +- 복잡한 비즈니스 요구사항 ëŒ€ì‘ +- 최신 기술 ë° íŒ¨í„´ ì ìš© +- 글로벌 확장 대비 + +## 3. 문서 작성 + +### 3.1 êµ¬ì¡°í™”ëœ ìž‘ì„± 순서 +1. **요구사항 ë¶„ì„ ê²°ê³¼** +2. **패턴 í‰ê°€** (í‰ê°€ 매트릭스 í¬í•¨) +3. **ì ìš© 설계** (Mermaid 다ì´ì–´ê·¸ëž¨) +4. **구현 계íš** (Phase별 로드맵) + +### 3.2 Mermaid 다ì´ì–´ê·¸ëž¨ 작성 +서비스 아키í…처와 패턴 ì ìš©ì„ 시ê°ì ìœ¼ë¡œ 표현합니다. + +```mermaid +graph TB + Client[í´ë¼ì´ì–¸íЏ] --> Gateway[API Gateway] + Gateway --> Auth[ì¸ì¦ 서비스] + Gateway --> UserSvc[ì‚¬ìš©ìž ì„œë¹„ìŠ¤] + Gateway --> OrderSvc[주문 서비스] + + OrderSvc --> EventBus[ì´ë²¤íЏ 버스] + EventBus --> PaymentSvc[ê²°ì œ 서비스] + EventBus --> NotificationSvc[알림 서비스] + + UserSvc --> UserDB[(ì‚¬ìš©ìž DB)] + OrderSvc --> OrderDB[(주문 DB)] + PaymentSvc --> PaymentDB[(ê²°ì œ DB)] +``` + +### 3.3 ì‹¤ìš©ì  ë‚´ìš© í¬í•¨ +- **코드 예시**: 패턴 êµ¬í˜„ì„ ìœ„í•œ 구체ì ì¸ 코드 스니펫 +- **구현 시 고려사항**: 실제 개발 시 주ì˜í•  ì  +- **ì˜ˆìƒ íš¨ê³¼**: ì •ëŸ‰ì  ì„±ê³¼ 지표 (ì‘답시간 개선, 처리량 ì¦ê°€ 등) + +## 참고 ìžë£Œ +- **유저스토리** +- UI/UX설계서 +- **í´ë¼ìš°ë“œì•„키í…처패턴요약표** + +## ê²°ê³¼ íŒŒì¼ +ì„ ì •ëœ ì•„í‚¤í…처 íŒ¨í„´ì€ ë‹¤ìŒê³¼ ê°™ì´ ë¬¸ì„œí™”ë©ë‹ˆë‹¤: + +### 파ì¼ëª… +design/pattern/architecture-pattern.md + +### 필수 í¬í•¨ ë‚´ìš© +1. **요구사항 ë¶„ì„ ê²°ê³¼** + - 기능ì /ë¹„ê¸°ëŠ¥ì  ìš”êµ¬ì‚¬í•­ ìƒì„¸ ë¶„ì„ + - ê¸°ìˆ ì  ë„전과제 ì‹ë³„ + +2. **패턴 ì„ ì • 매트릭스 (í‰ê°€í‘œ)** + - 후보 패턴별 ì •ëŸ‰ì  í‰ê°€ ì ìˆ˜ + - ì„ ì • 근거 ë° ì´ìœ  + +3. **서비스별 패턴 ì ìš© 설계 (Mermaid)** + - ì „ì²´ 아키í…처 구조 + - 패턴별 ì ìš© ì˜ì—­ 표시 + +4. **Phase별 구현 로드맵** + - 단계별 ì ìš© ê³„íš + - 마ì¼ìŠ¤í†¤ ë° ëª©í‘œ 설정 + +5. **ì˜ˆìƒ ì„±ê³¼ 지표** + - 성능 개선 예ìƒì¹˜ + - 비용 ì ˆê° íš¨ê³¼ + - 개발 ìƒì‚°ì„± í–¥ìƒ + +## ì²´í¬ë¦¬ìŠ¤íŠ¸ + +작성 완료 후 ë‹¤ìŒ í•­ëª©ë“¤ì„ ê²€í† í•˜ì„¸ìš”: + +- [ ] **ê° ìœ ì €ìŠ¤í† ë¦¬ê°€ ì–´ë–¤ 패턴으로 í•´ê²°ë˜ëŠ”ì§€ 명시했는가?** +- [ ] **패턴 ì„ ì • ì´ìœ ë¥¼ 정량ì ìœ¼ë¡œ 설명했는가?** +- [ ] **패턴 ê°„ ìƒí˜¸ìž‘용과 통합 아키í…처를 표현했는가?** +- [ ] **구현 우선순위와 단계별 목표가 명확한가?** +- [ ] **실무ìžê°€ 바로 활용할 수 있는 수준ì¸ê°€?** + +## 작성 시 주ì˜ì‚¬í•­ + +1. **ê°ê´€ì  í‰ê°€**: ì£¼ê´€ì  íŒë‹¨ë³´ë‹¤ëŠ” ì •ëŸ‰ì  ë°ì´í„° 기반 ì„ ì • +2. **현실성**: íŒ€ì˜ ê¸°ìˆ  수준과 프로ì íЏ ì¼ì •ì„ ê³ ë ¤í•œ 실현 가능한 패턴 ì„ ì • +3. **확장성**: 현재 요구사항ë¿ë§Œ ì•„ë‹ˆë¼ ë¯¸ëž˜ 확장성까지 ê³ ë ¤ +4. **비용 효율성**: ê³¼ë„한 ì—”ì§€ë‹ˆì–´ë§ ì§€ì–‘, 비즈니스 가치 중심 ì„ ì • +5. **문서화**: ì„ ì • 과정과 근거를 명확히 문서화하여 í›„ì† ì˜ì‚¬ê²°ì • ì§€ì› + +## 완료 후 mermaid 스í¬ë¦½íЏ 테스트 방법 안내 +- https://mermaid.live/edit ì— ì ‘ê·¼ +- 스í¬ë¦½íЏ ë‚´ìš©ì„ ë¶™ì—¬ë„£ì–´ í™•ì¸ \ No newline at end of file diff --git a/claude/cloud-design-patterns.md b/claude/cloud-design-patterns.md new file mode 100644 index 0000000..4e39358 --- /dev/null +++ b/claude/cloud-design-patterns.md @@ -0,0 +1,104 @@ +# í´ë¼ìš°ë“œ ë””ìžì¸ 패턴 개요 + +## ì „ì²´ 분류 현황 + +ì´ **42ê°œì˜ í´ë¼ìš°ë“œ ë””ìžì¸ 패턴** + +- **DB 성능개선**: 1ê°œ +- **ì½ê¸° 최ì í™”**: 4ê°œ +- **핵심업무 집중**: 6ê°œ +- **ì•ˆì •ì  í˜„ëŒ€í™”**: 2ê°œ +- **íš¨ìœ¨ì  ë¶„ì‚°ì²˜ë¦¬**: 13ê°œ +- **안정성**: 6ê°œ +- **보안**: 3ê°œ +- **ìš´ì˜**: 7ê°œ + +--- + +## 패턴 ëª©ë¡ + +### 1. DB 성능개선 (1ê°œ) + +| No. | 패턴명 | ëª©ì  | 설명 | +|-----|--------|------|------| +| 1 | Sharding | ë°ì´í„° ì–‘ 줄ì´ê¸° | ë°ì´í„° 저장소를 수í‰ì ìœ¼ë¡œ ë¶„í• (shard)하여 대규모 ë°ì´í„° 저장 ë° ì ‘ê·¼ 시 í™•ìž¥ì„±ì„ ë†’ì´ëŠ” 패턴 | + +### 2. ì½ê¸° 최ì í™” (4ê°œ) + +| No. | 패턴명 | ëª©ì  | 설명 | +|-----|--------|------|------| +| 2 | Index Table | NoSQL DB Query 최ì í™” | ë°ì´í„° 저장소ì—서 ìžì£¼ 참조ë˜ëŠ” í•„ë“œì— ëŒ€í•œ ì¸ë±ìŠ¤ë¥¼ ìƒì„±í•˜ì—¬ 쿼리 ì„±ëŠ¥ì„ ê°œì„ í•˜ëŠ” 패턴 | +| 3 | Cache-Aside | 성능 í–¥ìƒ ë° ë°ì´í„° ì¼ê´€ì„± 유지 | ë°ì´í„° 저장소ì—서 ìºì‹œì— ë°ì´í„°ë¥¼ í•„ìš”ì— ë”°ë¼ ë¡œë“œí•˜ì—¬ ì„±ëŠ¥ì„ ê°œì„ í•˜ê³ , ìºì‹œì™€ ë°ì´í„° 저장소 ê°„ì˜ ì¼ê´€ì„±ì„ 유지하는 패턴 | +| 4 | Materialized View | 쿼리 성능 최ì í™” | ë°ì´í„°ë¥¼ 미리 변환하여 ì¤€ë¹„ëœ ë·°ë¥¼ ìƒì„±í•¨ìœ¼ë¡œì¨ 쿼리 ì„±ëŠ¥ì„ ë†’ì´ê³  ë°ì´í„° ì¶”ì¶œì„ íš¨ìœ¨í™”í•˜ëŠ” 패턴 | +| 5 | CQRS | ì½ê¸°/쓰기 분리 | ë°ì´í„° ì €ìž¥ì†Œì˜ ì½ê¸°ì™€ 쓰기 ìž‘ì—…ì„ ë¶„ë¦¬í•˜ì—¬ 성능, 확장성, ë³´ì•ˆì„±ì„ ë†’ì´ëŠ” 패턴 | + +### 3. 핵심업무 집중 (6ê°œ) + +| No. | 패턴명 | ëª©ì  | 설명 | +|-----|--------|------|------| +| 6 | Gateway Offloading | 횡단관심사 분리 | SSL ì¸ì¦ì„œ 관리, ì¸ì¦, 로깅 ë“±ì˜ ê³µí†µ ê¸°ëŠ¥ì„ ê²Œì´íŠ¸ì›¨ì´ë¡œ 분리하여 애플리케ì´ì…˜ì˜ 복잡ë„를 낮추는 패턴 | +| 7 | Gateway Routing | ë¼ìš°íŒ… 중앙 처리 | ë‹¨ì¼ ì—”ë“œí¬ì¸íŠ¸ë¥¼ 통해 ìš”ì²­ì„ ë°›ì•„ 백엔드 서비스나 ì¸ìŠ¤í„´ìŠ¤ë¡œ ë¼ìš°íŒ…하는 패턴 | +| 8 | Gateway Aggregation | í´ë¼ì´ì–¸íЏ 요청 수 줄ì´ê¸° | ë‹¨ì¼ ì—”ë“œí¬ì¸íЏì—서 í´ë¼ì´ì–¸íЏ ìš”ì²­ì„ ë°›ì•„ 여러 백엔드 서비스로 분배하고 ì‘ë‹µì„ ì·¨í•©í•˜ëŠ” 패턴 | +| 9 | Backends for Frontends | 프론트엔드 유형별 전용처리 | 특정 í”„ëŸ°íŠ¸ì—”ë“œì— íŠ¹í™”ëœ ë°±ì—”ë“œ 서비스를 별ë„로 구축하는 패턴 | +| 10 | Sidecar | 공통 기능 분리 | 애플리케ì´ì…˜ì˜ ì¼ë¶€ ì»´í¬ë„ŒíŠ¸ë¥¼ ë³„ë„ í”„ë¡œì„¸ìŠ¤ë‚˜ 컨테ì´ë„ˆë¡œ 분리하여 격리와 í™•ìž¥ì„±ì„ ì œê³µí•˜ëŠ” 패턴 | +| 11 | Ambassador | ë„¤íŠ¸ì›Œí¬ í†µì‹ ì˜ ì•ˆì •ì„±ê³¼ 보안 ê°•í™” | í´ë¼ì´ì–¸íŠ¸ë¥¼ 대신해 ë„¤íŠ¸ì›Œí¬ ìš”ì²­ì„ ì²˜ë¦¬í•˜ëŠ” í—¬í¼ ì„œë¹„ìŠ¤ë¥¼ ìƒì„±í•˜ëŠ” 패턴 | + +### 4. ì•ˆì •ì  í˜„ëŒ€í™” (2ê°œ) + +| No. | 패턴명 | ëª©ì  | 설명 | +|-----|--------|------|------| +| 12 | Strangler Fig | í˜„ëŒ€í™”ì˜ ìœ„í—˜ 최소화와 ì ì§„ì  ì „í™˜ | 레거시 ì‹œìŠ¤í…œì„ ì ì§„ì ìœ¼ë¡œ 새로운 애플리케ì´ì…˜ ë° ì„œë¹„ìŠ¤ë¡œ êµì²´í•˜ëŠ” 패턴 | +| 13 | Anti-Corruption Layer | 시스템 ê°„ ì•ˆì •ì  ì¸í„°íŽ˜ì´ìФ | 서로 다른 하위 시스템 ê°„ì˜ ì˜ë¯¸ì  ì°¨ì´ë¥¼ 조정하기 위해 중간 ê³„ì¸µì„ êµ¬í˜„í•˜ëŠ” 패턴 | + +### 5. íš¨ìœ¨ì  ë¶„ì‚°ì²˜ë¦¬ (13ê°œ) + +| No. | 패턴명 | ëª©ì  | 설명 | +|-----|--------|------|------| +| 14 | Pipes and Filters | 작업 단계 모듈화로 재사용성과 성능 í–¥ìƒ | 복잡한 ìž‘ì—…ì„ ë…립ì ì¸ 단계(í•„í„°)로 분리하고 메시지(파ì´í”„)로 연결하여 모듈성과 ìœ ì—°ì„±ì„ ë†’ì´ëŠ” 패턴 | +| 15 | Scheduler Agent Supervisor | 워í¬í”Œë¡œìš°ì˜ 신뢰성 í–¥ìƒ | 작업 단계를 스케줄러, ì—ì´ì „트, ê°ë…ìžë¡œ 분리하여 신뢰성과 í™•ìž¥ì„±ì„ ë†’ì´ëŠ” 패턴 | +| 16 | Leader Election | ë¶„ì‚° ìž‘ì—…ì˜ ì¶©ëŒ ë°©ì§€ì™€ 안정성 í–¥ìƒ | ë¶„ì‚° 시스템ì—서 여러 작업 ì¸ìŠ¤í„´ìŠ¤ 중 하나를 리ë”로 선출하여 ì¡°ì • ì—­í• ì„ ë§¡ê¸°ëŠ” 패턴 | +| 17 | Saga | ë°ì´í„° ì¼ê´€ì„± 보장 | ê° ì„œë¹„ìŠ¤ì˜ ë¡œì»¬ íŠ¸ëžœìž­ì…˜ì„ ì‚¬ìš©í•˜ì—¬ ë¶„ì‚° íŠ¸ëžœìž­ì…˜ì˜ ì¼ê´€ì„±ì„ 보장하는 패턴 | +| 18 | Compensating Transaction | 오류 복구로 ë°ì´í„° ì¼ê´€ì„± 보장 | ë¶„ì‚° 트랜잭션ì—서 실패한 ìž‘ì—…ì„ ë³´ìƒí•˜ê¸° 위해 ì´ì „ ìž‘ì—…ì„ ì·¨ì†Œí•˜ê±°ë‚˜ ìƒì‡„하는 íŠ¸ëžœìž­ì…˜ì„ ì‹¤í–‰í•˜ëŠ” 패턴 | +| 19 | Priority Queue | 중요 ìž‘ì—…ì˜ ìš°ì„  처리 보장 | ë©”ì‹œì§€ì˜ ìš°ì„ ìˆœìœ„ì— ë”°ë¼ ì²˜ë¦¬ 순서를 조정하는 í를 사용하는 패턴 | +| 20 | Queue-Based Load Leveling | ë¶€í•˜ì˜ ê· ë“±í•œ 분산으로 안정성 확보 | 메시지 í를 사용하여 작업과 서비스 ê°„ì˜ ë¶€í•˜ë¥¼ 균등하게 분산시키는 패턴 | +| 21 | Sequential Convoy | 처리순서 보장 | 관련 메시지 ì§‘í•©ì„ ìˆœì„œëŒ€ë¡œ ì²˜ë¦¬í•˜ë˜ ë‹¤ë¥¸ 메시지 처리를 차단하지 않ë„ë¡ í•˜ëŠ” 패턴 | +| 22 | Claim Check | 메시지 í¬ê¸° 최소화 ë° ì„±ëŠ¥ê³¼ 보안 í–¥ìƒ | 메시지ì—서 페ì´ë¡œë“œë¥¼ 분리하여 외부 ì €ìž¥ì†Œì— ì €ìž¥í•˜ê³  참조키(í´ë ˆìž„ ì²´í¬)를 사용하는 패턴 | +| 23 | Publisher-Subscriber | ë‹¨ì¼ ì´ë²¤íЏ ë©”ì‹œì§€ì˜ ë³µìˆ˜ 서비스 처리 보장 | ë‹¤ìˆ˜ì˜ ì†Œë¹„ìž(Consumer)ì—게 ì´ë²¤íŠ¸ë¥¼ 발행하는 패턴 | +| 24 | Asynchronous Request-Reply | 장시간 처리 ìž‘ì—…ì˜ ì‘답시간 단축 | 프런트엔드(í´ë¼ì´ì–¸íЏ)와 백엔드 ê°„ 비ë™ê¸°ë¡œ 요청과 ì‘ë‹µì„ ë¶„ë¦¬í•˜ì—¬ ì‘답 ì‹œê°„ì„ ë‹¨ì¶•í•˜ëŠ” 패턴 | +| 25 | Competing Consumers | 병렬처리로 작업 처리 ì†ë„ í–¥ìƒ | ë™ì¼ 메시지 채ë„ì—서 여러 소비ìžê°€ ê²½ìŸì ìœ¼ë¡œ 메시지를 처리하여 ë³‘ë ¬ì„±ì„ ë†’ì´ëŠ” 패턴 | +| 26 | Choreography | 중앙집중 ì²˜ë¦¬ì˜ ë³‘ëª©í˜„ìƒ ë°©ì§€ | 중앙 ì¡°ì •ìž ì—†ì´ ê° ì„œë¹„ìŠ¤ê°€ ìžìœ¨ì ìœ¼ë¡œ ì´ë²¤íŠ¸ë¥¼ 구ë…하고 ë°˜ì‘하여 ì „ì²´ 워í¬í”Œë¡œë¥¼ 수행하는 패턴 | + +### 6. 안정성 (6ê°œ) + +| No. | 패턴명 | ëª©ì  | 설명 | +|-----|--------|------|------| +| 27 | Rate Limiting | 요청 í­ì£¼ 방지로 안정성 유지 | ì¼ì • 기간 ë™ì•ˆ 허용ë˜ëŠ” 요청 수를 제한하여 과부하를 방지하고 서비스 ì•ˆì •ì„±ì„ ë†’ì´ëŠ” 패턴 | +| 28 | Throttling | 요청 í­ì£¼ 방지로 안정성 유지 | ì‹œìŠ¤í…œì˜ ë¶€í•˜ ìƒíƒœì— ë”°ë¼ ìš”ì²­ ì²˜ë¦¬ëŸ‰ì„ ë™ì ìœ¼ë¡œ 조절하여 과부하를 방지하는 패턴 | +| 29 | Bulkhead | ìžì›í’€ 격리로 장애 전파 ë°©ì§€ | 애플리케ì´ì…˜ 요소를 ê²©ë¦¬ëœ í’€ë¡œ 분할하여 í•˜ë‚˜ì˜ ìž¥ì• ê°€ 전체로 전파ë˜ëŠ” ê²ƒì„ ë°©ì§€í•˜ëŠ” 패턴 | +| 30 | Circuit Breaker | 장애전파 ë°©ì§€ | 장애가 ë°œìƒí•œ 구성 요소를 빠르게 ê°ì§€í•˜ê³  요청 실패를 최소화하는 패턴 | +| 31 | Retry | ì¼ì‹œì  오류시 처리 보장 | ì¼ì‹œì ì¸ ì˜¤ë¥˜ì— ëŒ€í•´ 실패한 ìš”ì²­ì„ ìž¬ì‹œë„하여 ë³µì›ë ¥ì„ 높ì´ëŠ” 패턴 | +| 32 | Event Sourcing | ë°ì´í„° 멱등성 보장과 변경 ê¸°ë¡ ì œê³µ | ë°ì´í„°ì— 대한 모든 ë³€ê²½ì‚¬í•­ì„ ì´ë²¤íŠ¸ë¡œ 저장하고, ì´ë²¤íŠ¸ë¥¼ 재ìƒí•˜ì—¬ ë°ì´í„°ì˜ ìƒíƒœë¥¼ ë³µì›í•˜ëŠ” 패턴 | + +### 7. 보안 (3ê°œ) + +| No. | 패턴명 | ëª©ì  | 설명 | +|-----|--------|------|------| +| 33 | Federated Identity | ì‚¬ìš©ìž ì¸ì¦ ë° ê´€ë¦¬ 효율화 | ì¸ì¦ì„ 외부 ID 제공ìžì— 위임하여 ì‚¬ìš©ìž ê´€ë¦¬ë¥¼ 간소화하고 SSO를 구현하는 패턴 | +| 34 | Gatekeeper | ë°ì´í„° ì ‘ê·¼ 제어와 보안 ê°•í™” | 신뢰할 수 있는 í˜¸ìŠ¤íŠ¸ì— ë³´ì•ˆ 관련 ê¸°ëŠ¥ì„ ì§‘ì¤‘ì‹œì¼œ 스토리지나 ì„œë¹„ìŠ¤ì˜ ë³´ì•ˆì„ ê°•í™”í•˜ëŠ” 패턴 | +| 35 | Valet Key | ë„¤íŠ¸ì›Œí¬ ëŒ€ì—­í­ ê°ì†Œ | í´ë¼ì´ì–¸íŠ¸ê°€ 특정 ë¦¬ì†ŒìŠ¤ì— ì œí•œëœ ì§ì ‘ ì ‘ê·¼ì„ í•  수 있ë„ë¡ í† í°ì„ 사용하는 패턴 | + +### 8. ìš´ì˜ (7ê°œ) + +| No. | 패턴명 | ëª©ì  | 설명 | +|-----|--------|------|------| +| 36 | Geodes | 글로벌 서비스 가용성과 성능 최ì í™” | 백엔드 서비스를 여러 ì§€ì—­ì— ë¶„ì‚° 배치하여 지연 ì‹œê°„ì„ ì¤„ì´ê³  ê°€ìš©ì„±ì„ ë†’ì´ëŠ” 패턴 | +| 37 | Deployment Stamps | 멀티 테넌트 관리 | 리소스 ê·¸ë£¹ì„ ë³µì œí•˜ì—¬ 작업ì´ë‚˜ 테넌트 단위로 ê²©ë¦¬ëœ ìš´ì˜ í™˜ê²½ì„ ì œê³µí•˜ëŠ” 패턴 | +| 38 | Health Endpoint Monitoring | 서비스 가용성 ìƒíƒœ ì ê²€ | 애플리케ì´ì…˜ì˜ ìƒíƒœë¥¼ 모니터ë§í•˜ê¸° 위한 ì „ìš© API 엔드í¬ì¸íŠ¸ë¥¼ 노출하는 패턴 | +| 39 | Compute Resource Consolidation | ìžì› 사용 효율성과 비용 ì ˆê° | 여러 작업ì´ë‚˜ ìš´ì˜ì„ ë‹¨ì¼ ì»´í“¨íŒ… 단위로 통합하여 효율성과 ë¹„ìš©ì„ ìµœì í™”하는 패턴 | +| 40 | Static Content Hosting | ì •ì  ìžì› 제공, 비용절ê°ê³¼ 성능 í–¥ìƒ | ì •ì  ì½˜í…츠를 í´ë¼ìš°ë“œ ìŠ¤í† ë¦¬ì§€ì— ë°°í¬í•˜ì—¬ í´ë¼ì´ì–¸íŠ¸ì— ì§ì ‘ ì œê³µí•¨ìœ¼ë¡œì¨ ì»´í“¨íŒ… ì¸ìŠ¤í„´ìŠ¤ ì‚¬ìš©ì„ ì¤„ì´ëŠ” 패턴 | +| 41 | External Configuration Store | 환경설정 중앙관리와 ìž¬ë°°í¬ ì—†ì´ ì„¤ì • 변경 ì ìš© | 애플리케ì´ì…˜ì˜ 설정 정보를 중앙화하여 관리 íš¨ìœ¨ì„±ì„ ë†’ì´ê³  설정 ê°’ 변경 시 ìž¬ë°°í¬ ì—†ì´ ì ìš©í•˜ëŠ” 패턴 | +| 42 | Edge Workload Configuration | ì—£ì§€ì»´í“¨íŒ…ì˜ íš¨ìœ¨ì  ê´€ë¦¬ | 장치와 ì‹œìŠ¤í…œì´ í˜¼ìž¬ëœ ì—£ì§€ 환경ì—서 워í¬ë¡œë“œ êµ¬ì„±ì„ íš¨ìœ¨ì ìœ¼ë¡œ 관리하여 지연 시간 단축과 ë„¤íŠ¸ì›Œí¬ ë¹„ìš© ì ˆê°ì„ 하는 패턴 | + +--- + +> **참고**: ì´ ë¬¸ì„œëŠ” í´ë¼ìš°ë“œ 환경ì—서 ìžì£¼ 사용ë˜ëŠ” ë””ìžì¸ íŒ¨í„´ë“¤ì„ ì²´ê³„ì ìœ¼ë¡œ 분류하여 정리한 것입니다. ê° íŒ¨í„´ì€ íŠ¹ì • 목ì ê³¼ ìƒí™©ì— 맞게 ì ìš©ë  수 있으며, 실제 구현 시ì—는 프로ì íŠ¸ì˜ ìš”êµ¬ì‚¬í•­ê³¼ ì œì•½ì‚¬í•­ì„ ì¶©ë¶„ížˆ 고려해야 합니다. diff --git a/claude/common-principles.md b/claude/common-principles.md new file mode 100644 index 0000000..31b8354 --- /dev/null +++ b/claude/common-principles.md @@ -0,0 +1,197 @@ +# 공통설계ì›ì¹™ + +모든 설계 단계ì—서 공통으로 ì ìš©ë˜ëŠ” 핵심 ì›ì¹™ê³¼ ì „ëžµ + +## 🎯 핵심 ì›ì¹™ + +### 1. 🚀 실행 ìš°ì„  ì›ì¹™ +- **프롬프트 ìš°ì„ **: 바로 실행할 수 있는 프롬프트로 작업 시작 +- **ê°€ì´ë“œ 학습**: ì›ë¦¬ì™€ ë°©ë²•ë¡ ì€ ê°€ì´ë“œë¡œ ê¹Šì´ ìžˆê²Œ 학습 +- **ì ì§„ì  ì´í•´**: 실행 → ê²°ê³¼ í™•ì¸ â†’ ì›ë¦¬ 학습 순서 + +### 2. 🔄 병렬 처리 ì „ëžµ +- **서브 ì—ì´ì „트 활용**: Task ë„구로 서비스별 ë™ì‹œ 작업 +- **ì˜ì¡´ì„± 기반 그룹화**: ì˜ì¡´ ê´€ê³„ì— ë”°ë¥¸ 순차/병렬 처리 ê²°ì • +- **통합 ê²€ì¦**: 병렬 작업 완료 후 ì „ì²´ì  ì¼ê´€ì„± ê²€ì¦ + +### 3. ðŸ—ï¸ ë§ˆì´í¬ë¡œì„œë¹„스 설계 ì›ì¹™ +- **서비스 ë…립성**: ìºì‹œë¥¼ 통한 ì§ì ‘ ì˜ì¡´ì„± 최소화 +- **ì„ íƒì  비ë™ê¸°**: 장시간 작업(AI ì¼ì • ìƒì„±)ë§Œ 비ë™ê¸° 처리 +- **ìºì‹œ ìš°ì„ **: Redis를 통한 성능 최ì í™” +- **ë…립 ë°°í¬**: 서비스별 ë…ë¦½ì  ë°°í¬ ê°€ëŠ¥í•œ 구조 + +### 4. 📠표준화 ì›ì¹™ +- **PlantUML**: 모든 다ì´ì–´ê·¸ëž¨ 표준 (`!theme mono`) +- **OpenAPI 3.0**: API 명세 표준 +- **ìžë™ ê²€ì¦**: PlantUML, OpenAPI 문법 검사 필수 +- **ì¼ê´€ëœ 네ì´ë°**: kebab-case 파ì¼ëª…, í‘œì¤€í™”ëœ ìŠ¤í‚¤ë§ˆëª… + +### 5. ✅ ê²€ì¦ ìš°ì„  ì›ì¹™ +- **ê° ë‹¨ê³„ë§ˆë‹¤ ìžë™ ê²€ì¦**: 품질 보장 +- **실시간 피드백**: 오류 조기 발견 ë° ìˆ˜ì • +- **CI/CD 통합**: ìžë™í™”ëœ ê²€ì¦ í”„ë¡œì„¸ìŠ¤ +- **PlantUML 스í¬ë¦½íЏ íŒŒì¼ ìƒì„± 즉시 검사 실행**: 'PlantUML 문법 검사 ê°€ì´ë“œ' 준용 + +### 6. 🚀 ì ì§„ì  êµ¬í˜„ ì›ì¹™ +- **MVP → 확장 → ê³ ë„í™”**: 단계별 ì ‘ê·¼ +- **YAGNI ì ìš©**: ê¼­ 필요한 기능만 구현(YAGNIì›ì¹™:You aren't gonna need it) +- **ì§€ì†ì  개선**: 피드백 기반 ì ì§„ì  ë°œì „ + +## 🔧 ì˜ì¡´ì„± ë¶„ì„ ë° ë³‘ë ¬ 처리 + +### ì˜ì¡´ì„± ë¶„ì„ ë°©ë²• + +1. **서비스 ê°„ ì˜ì¡´ì„± 파악** + ``` + ì¼ì • 서비스 → í”„ë¡œíŒŒì¼ ì„œë¹„ìŠ¤ (멤버/여행 ì •ë³´ 조회) + ì¼ì • 서비스 → 장소 서비스 (장소 ì •ë³´ 조회) + 장소 서비스: ë…ë¦½ì  (외부 APIë§Œ 사용) + ``` + +2. **ì˜ì¡´ì„± 기반 그룹화** + ``` + Group A (순차 처리): í”„ë¡œíŒŒì¼ â†’ ì¼ì • 서비스 + Group B (ë…립 처리): 장소 서비스 + ``` + +3. **ì—ì´ì „트 할당 ë° ë³‘ë ¬ 처리** + ``` + Agent 1: Group A 담당 + - í”„ë¡œíŒŒì¼ ì„œë¹„ìŠ¤ 설계 + - ì¼ì • 서비스 설계 (í”„ë¡œíŒŒì¼ ì°¸ì¡°) + + Agent 2: Group B 담당 + - 장소 서비스 설계 (ë…립ì ) + ``` + +### 병렬 처리 ì ìš© ê°€ì´ë“œ + +#### API 설계 단계 +- **ë…립 서비스**: ê°ê° ë³„ë„ ì—ì´ì „트 +- **ì˜ì¡´ 서비스**: ë™ì¼ ì—ì´ì „트 ë‚´ 순차 처리 +- **공통 ê²€ì¦**: 모든 ì—ì´ì „트 완료 후 swagger-cli ê²€ì¦ + +#### 시퀀스 설계 단계 +- **외부 시퀀스**: 플로우별 병렬 처리 (ê° í”Œë¡œìš°ëŠ” ë…립ì ) +- **ë‚´ë¶€ 시퀀스**: 서비스별 병렬 처리 (서비스 내부는 ë…립ì ) + +#### í´ëž˜ìФ/ë°ì´í„° 설계 단계 +- **ì˜ì¡´ì„± 그룹별**: 참조 관계가 있는 ì„œë¹„ìŠ¤ë“¤ì€ ìˆœì°¨ 처리 +- **ë…립 서비스**: 병렬 처리 +- **공통 í´ëž˜ìФ**: 모든 서비스 설계 완료 후 마지막 처리 + +## 🎨 PlantUML 작성 표준 + +### 기본 템플릿 +```plantuml +@startuml +!theme mono + +title [다ì´ì–´ê·¸ëž¨ 제목] + +' 다ì´ì–´ê·¸ëž¨ ë‚´ìš© +@enduml +``` + +### 필수 ê²€ì¦ +- **ê° íŒŒì¼ ìƒì„± ì§í›„ PlantUML 문법 검사 수행** +- **파ì´í”„ ë°©ì‹ ê¶Œìž¥**: `cat "파ì¼" | docker exec -i plantuml ...` +- **화살표 문법 주ì˜**: sequence diagramì—서 `..>` 사용 금지 + +### ìŠ¤íƒ€ì¼ ê°€ì´ë“œ +- **í°íЏ í¬ê¸°**: ì ì ˆí•œ ê°€ë…성 확보 +- **ìƒ‰ìƒ êµ¬ë¶„**: 서비스별, ë ˆì´ì–´ë³„ ìƒ‰ìƒ êµ¬ë¶„ +- **설명 추가**: note를 활용한 ìƒì„¸ 설명 + +## 🔌 API 설계 표준 + +### íŒŒì¼ êµ¬ì¡° +``` +design/backend/api/{service-name}-api.yaml +``` + +### 필수 필드 +```yaml +paths: + /endpoint: + get: + summary: API ëª©ì  ì„¤ëª… + operationId: 고유 ì‹ë³„ìž + x-user-story: 유저스토리 ID + x-controller: 담당 컨트롤러 + tags: [API 그룹] +``` + +### 스키마 ì›ì¹™ +- **서비스별 ë…립**: ê° ì„œë¹„ìŠ¤ 파ì¼ì— 모든 스키마 í¬í•¨ +- **중복 허용**: 초기ì—는 ì¤‘ë³µì„ í—ˆìš©í•˜ê³  ì ì§„ì ìœ¼ë¡œ 공통화 +- **명확한 네ì´ë°**: Request/Response DTO 네ì´ë° 표준 + +## 🔄 시퀀스 설계 표준 + +### 외부 시퀀스 (서비스 ê°„) +- **참여 서비스만**: 해당 í”Œë¡œìš°ì— ì°¸ì—¬í•˜ëŠ” 서비스만 표시 +- **API 호출 중심**: 서비스 ê°„ API 호출 순서 표현 +- **한글 설명**: ê° í˜¸ì¶œì˜ ëª©ì ì„ 한글로 명시 + +### ë‚´ë¶€ 시퀀스 (서비스 ë‚´ë¶€) +- **모든 API 표시**: 해당 ì„œë¹„ìŠ¤ì˜ ëª¨ë“  API í¬í•¨ +- **ë‚´ë¶€ 처리 í름**: 컨트롤러 → 서비스 → ë ˆí¬ì§€í† ë¦¬ 플로우 +- **ê¸°ìˆ ì  ì„¸ë¶€ì‚¬í•­**: ìºì‹œ, DB ì ‘ê·¼ 등 í¬í•¨ + +### ë™ê¸°/비ë™ê¸° 구분 +- **실선 (→)**: ë™ê¸° 호출 +- **ì ì„  (->>)**: 비ë™ê¸° 호출 +- **ì–‘ë°©í–¥**: 필요시ì—ë§Œ 사용 + +## 📊 í´ëž˜ìФ 설계 표준 + +### 아키í…처 ì ìš© +- **Clean Architecture**: Port/Adapter 용어 대신 표준 Clean 용어 사용 +- **멀티프로ì íЏ**: 서비스별 ë…립 모듈 +- **패키지 구조 표준**: ì¼ê´€ëœ 패키지 구조 ì ìš© + +### 관계 표현 +- **Generalization**: ìƒì† 관계 +- **Realization**: ì¸í„°íŽ˜ì´ìФ 구현 +- **Dependency**: ì˜ì¡´ 관계 +- **Association**: ì—°ê´€ 관계 +- **Aggregation/Composition**: 집약/합성 관계 + +### ìƒì„¸ ì •ë³´ +- **프로í¼í‹°ì™€ 메소드**: ëª¨ë‘ ëª…ì‹œ +- **ì ‘ê·¼ 제한ìž**: ì ì ˆí•œ 가시성 설정 +- **타입 ì •ë³´**: 정확한 ë°ì´í„° 타입 명시 + +## ðŸ—„ï¸ ë°ì´í„° 설계 표준 + +### 서비스별 DB 분리 +- **ê° ì„œë¹„ìŠ¤ë§ˆë‹¤ ë…립ì ì¸ ë°ì´í„°ë² ì´ìФ** +- **서비스 ê°„ ë°ì´í„° 공유 최소화** +- **ìºì‹œë¥¼ 통한 성능 최ì í™”** + +### í´ëž˜ìФ 설계와 ì¼ì¹˜ +- **Entity í´ëž˜ìŠ¤ì™€ í…Œì´ë¸” 매핑** +- **ì¼ê´€ëœ 네ì´ë° 컨벤션** +- **ì ì ˆí•œ 정규화 수준** + +## 🚨 주ì˜ì‚¬í•­ + +### PlantUML 문법 +- **sequence diagramì—서 `..>` 사용 금지** +- **비ë™ê¸°ëŠ” `->>` ë˜ëŠ” `-->>` 사용** +- **테마는 반드시 `mono` 사용** + +### 병렬 처리 +- **ì˜ì¡´ì„± ë¶„ì„ ì„ í–‰**: 병렬 처리 ì „ 반드시 ì˜ì¡´ì„± 파악 +- **순차 처리 필요시**: 무리한 병렬화보다는 안전한 순차 처리 +- **ê²€ì¦ ë‹¨ê³„ 필수**: 병렬 처리 후 통합 ê²€ì¦ + +### API 설계 +- **유저스토리 ID 필수**: x-user-story 필드 ëˆ„ë½ ê¸ˆì§€ +- **Controller 명시**: x-controller 필드로 담당 컨트롤러 명시 +- **스키마 완전성**: 모든 Request/Response 스키마 ì •ì˜ + +--- + +💡 **ì´ ì›ì¹™ë“¤ì€ 모든 설계 단계ì—서 ì¼ê´€ë˜ê²Œ ì ìš©ë˜ì–´ì•¼ 하며, ê° ë‹¨ê³„ë³„ 세부 ê°€ì´ë“œì—서 구체ì ìœ¼ë¡œ 구현ë©ë‹ˆë‹¤.** \ No newline at end of file diff --git a/claude/conversation-summary.md b/claude/conversation-summary.md new file mode 100644 index 0000000..acaca05 --- /dev/null +++ b/claude/conversation-summary.md @@ -0,0 +1,823 @@ +# ë‚´ë¶€ 시퀀스 설계 대화 ìƒì„¸ 요약 + +## 1. 주요 요청 ë° ì˜ë„ (Primary Request and Intent) + +사용ìžëŠ” ë‹¤ìŒ ëª…ë ¹ì–´ë¥¼ 통해 **ë‚´ë¶€ 시퀀스 설계**(Internal Sequence Design)를 요청했습니다: + +``` +/design-seq-inner @architecture +ë‚´ë¶€ 시퀀스 설계를 í•´ 주세요: +- '공통설계ì›ì¹™'ê³¼ '내부시퀀스설계 ê°€ì´ë“œ'를 준용하여 설계 +``` + +### êµ¬ì²´ì  ìš”êµ¬ì‚¬í•­: +- **공통설계ì›ì¹™**(Common Design Principles) 준수 +- **내부시퀀스설계 ê°€ì´ë“œ**(Internal Sequence Design Guide) 준수 +- 7ê°œ 마ì´í¬ë¡œì„œë¹„ìŠ¤ì˜ ë‚´ë¶€ 처리 í름 설계 +- PlantUML 시퀀스 다ì´ì–´ê·¸ëž¨ 작성 (Controller → Service → Repository ë ˆì´ì–´) +- Resilience 패턴 ì ìš© (Circuit Breaker, Retry, Timeout, Fallback, Bulkhead) +- Cache, DB, 외부 API ìƒí˜¸ìž‘ìš© 표시 +- 외부 ì‹œìŠ¤í…œì€ `<>` 마킹 +- PlantUML íŒŒì¼ ìƒì„± 즉시 문법 검사 실행 + +--- + +## 2. 주요 기술 ê°œë… (Key Technical Concepts) + +### 아키í…처 패턴 + +**Event-Driven Architecture** +- Apache Kafka를 통한 비ë™ê¸° 메시징 +- ì´ë²¤íЏ 토픽: `EventCreated`, `ParticipantRegistered`, `WinnerSelected`, `DistributionCompleted` +- Job 토픽: `ai-job`, `image-job` +- At-Least-Once 전달 보장 + Redis Setì„ í†µí•œ 멱등성 보장 + +**CQRS (Command Query Responsibility Segregation)** +- Command: ì´ë²¤íЏ ìƒì„±, 참여 등ë¡, ë‹¹ì²¨ìž ì¶”ì²¨ +- Query: 대시보드 조회, ì´ë²¤íЏ 목ë¡/ìƒì„¸ 조회 +- ì½ê¸°/쓰기 분리로 성능 최ì í™” + +**Microservices Architecture (7ê°œ ë…립 서비스)** +1. **User Service**: íšŒì› ê´€ë¦¬ (가입, 로그ì¸, 프로필, 로그아웃) +2. **Event Service**: ì´ë²¤íЏ ìƒì„± 플로우 (10ê°œ 시나리오) +3. **AI Service**: 트렌드 ë¶„ì„ ë° ì¶”ì²œ +4. **Content Service**: ì´ë¯¸ì§€ ìƒì„± +5. **Distribution Service**: 다중 ì±„ë„ ë°°í¬ +6. **Participation Service**: ê³ ê° ì°¸ì—¬ 관리 +7. **Analytics Service**: 성과 ë¶„ì„ + +**Layered Architecture** +- **API Layer**: 모든 REST 엔드í¬ì¸íЏ +- **Business Layer**: Controller → Service → Domain ë¡œì§ +- **Data Layer**: Repository, Cache, External API +- **Infrastructure Layer**: Kafka, Logging, Monitoring + +### Resilience 패턴 ìƒì„¸ + +**1. Circuit Breaker Pattern** +- **구현**: Resilience4j ë¼ì´ë¸ŒëŸ¬ë¦¬ +- **임계값**: 50% 실패율 → OPEN ìƒíƒœ 전환 +- **ì ìš© 대ìƒ**: + - 국세청 API (사업ìžë²ˆí˜¸ ê²€ì¦) + - Claude/GPT-4 API (AI 추천) + - Stable Diffusion/DALL-E API (ì´ë¯¸ì§€ ìƒì„±) + - 외부 ì±„ë„ API (우리ì€í–‰, 지니뮤ì§, SNS 등) +- **OPEN ìƒíƒœ**: 10ì´ˆ 대기 후 HALF_OPEN으로 전환 +- **í´ë°±**: ìºì‹œ ë°ì´í„°, 기본값, ê²€ì¦ ìŠ¤í‚µ + +**2. Retry Pattern** +- **최대 재시ë„**: 3회 +- **백오프 ì „ëžµ**: Exponential backoff (1ì´ˆ, 2ì´ˆ, 4ì´ˆ) +- **ì ìš© 시나리오**: + - 외부 API ì¼ì‹œì  장애 + - ë„¤íŠ¸ì›Œí¬ íƒ€ìž„ì•„ì›ƒ + - 429 Too Many Requests ì‘답 + +**3. Timeout Pattern** +- **서비스별 타임아웃**: + - 국세청 API: 5ì´ˆ + - AI 추천 API: 30ì´ˆ (복잡한 처리) + - ì´ë¯¸ì§€ ìƒì„± API: 20ì´ˆ + - ë°°í¬ ì±„ë„ API: 10ì´ˆ +- **목ì **: 무한 대기 ë°©ì§€, 리소스 효율 관리 + +**4. Fallback Pattern** +- **전략별 í´ë°±**: + - 사업ìžë²ˆí˜¸ ê²€ì¦ ì‹¤íŒ¨ → ìºì‹œ ë°ì´í„° 사용 (TTL 7ì¼) + - AI 추천 실패 → 기본 템플릿 추천 + - ì´ë¯¸ì§€ ìƒì„± 실패 → Stable Diffusion → DALL-E → 템플릿 ì´ë¯¸ì§€ + - ì±„ë„ ë°°í¬ ì‹¤íŒ¨ → 다른 ì±„ë„ ê³„ì† ì§„í–‰ (ë…립 처리) + +**5. Bulkhead Pattern** +- **ë…립 스레드 í’€**: 채ë„별 격리 +- **목ì **: 한 ì±„ë„ ìž¥ì• ê°€ 다른 채ë„ì— ì˜í–¥ ì—†ìŒ +- **ì ìš©**: Distribution Service 다중 ì±„ë„ ë°°í¬ + - 우리ì€í–‰ ì˜ìƒ ê´‘ê³  + - ë§ê³  벨소리/ì»¬ëŸ¬ë§ + - ì§€ë‹ˆë®¤ì§ TV ê´‘ê³  + - Instagram 피드 + - 네ì´ë²„ 블로그 + - 카카오 ì±„ë„ + +### ìºì‹± ì „ëžµ + +**Cache-Aside Pattern (Redis)** + +| ë°ì´í„° 유형 | TTL | 히트율 목표 | ì ìš© 시나리오 | +|------------|-----|------------|-------------| +| 사업ìžë²ˆí˜¸ ê²€ì¦ | 7ì¼ | 95% | User Service 회ì›ê°€ìž… | +| AI 추천 ê²°ê³¼ | 24시간 | 80% | AI Service 트렌드 ë¶„ì„ | +| ì´ë¯¸ì§€ ìƒì„± ê²°ê³¼ | 7ì¼ | 90% | Content Service ì´ë¯¸ì§€ ìƒì„± | +| 대시보드 통계 | 5ë¶„ | 70% | Analytics Service 대시보드 | +| ì´ë²¤íЏ ìƒì„¸ | 5ë¶„ | 85% | Event Service ìƒì„¸ 조회 | +| ì´ë²¤íЏ ëª©ë¡ | 1ë¶„ | 60% | Event Service ëª©ë¡ ì¡°íšŒ | +| ì°¸ì—¬ìž ëª©ë¡ | 5ë¶„ | 75% | Participation Service ëª©ë¡ ì¡°íšŒ | + +**ìºì‹œ 무효화 ì „ëžµ**: +- ì´ë²¤íЏ ìƒì„±/수정 → ì´ë²¤íЏ ìºì‹œ ì‚­ì œ +- ì°¸ì—¬ìž ë“±ë¡ â†’ ì°¸ì—¬ìž ëª©ë¡ ìºì‹œ ì‚­ì œ +- ë‹¹ì²¨ìž ì¶”ì²¨ → ì´ë²¤íЏ/ì°¸ì—¬ìž ìºì‹œ ì‚­ì œ +- ë°°í¬ ì™„ë£Œ → 대시보드 ìºì‹œ ì‚­ì œ + +### 보안 (Security) + +**Password Hashing** +- **알고리즘**: bcrypt +- **Cost Factor**: 10 +- **ê²€ì¦**: Service 계층ì—서 수행 + +**ë¯¼ê° ë°ì´í„° 암호화** +- **알고리즘**: AES-256 +- **대ìƒ**: 사업ìžë²ˆí˜¸ +- **키 관리**: 환경 변수 (ENCRYPTION_KEY) + +**JWT 토í°** +- **만료 시간**: 7ì¼ +- **저장 위치**: Redis Session +- **로그아웃**: Blacklistì— ì¶”ê°€ (TTL = ë‚¨ì€ ë§Œë£Œ 시간) + +**ë°ì´í„° 마스킹** +- **전화번호**: `010-****-1234` +- **ì ìš©**: ì°¸ì—¬ìž ëª©ë¡ ì¡°íšŒ + +--- + +## 3. íŒŒì¼ ë° ì½”ë“œ 섹션 (Files and Code Sections) + +### 다운로드한 ê°€ì´ë“œ 문서 + +**1. claude/common-principles.md** +- **ë‚´ìš©**: 공통 설계 ì›ì¹™, PlantUML 표준, API 설계 표준 +- **주요 ì›ì¹™**: + - 실행 ìš°ì„  ì›ì¹™ + - 병렬 처리 ì „ëžµ + - 마ì´í¬ë¡œì„œë¹„스 설계 ì›ì¹™ + - 표준화 ì›ì¹™ + - ê²€ì¦ ìš°ì„  ì›ì¹™ + - ì ì§„ì  êµ¬í˜„ ì›ì¹™ + +**2. claude/sequence-inner-design.md** +- **ë‚´ìš©**: ë‚´ë¶€ 시퀀스 설계 ê°€ì´ë“œ, 시나리오 분류 ê°€ì´ë“œ +- **주요 ë‚´ìš©**: + - 작성 ì›ì¹™: 유저스토리 매칭, 외부 시퀀스 ì¼ì¹˜, 서비스별 분리 + - 표현 요소: API/Business/Data/Infrastructure ë ˆì´ì–´ + - 작성 순서: 준비 → 실행 → 검토 + - 시나리오 분류: 유저스토리 기반, 비즈니스 기능 단위 + - 병렬 수행: 서브 ì—ì´ì „트 활용 + +### 참조 문서 + +**design/userstory.md (999줄)** +- **20ê°œ 유저스토리** (7ê°œ 마ì´í¬ë¡œì„œë¹„스) +- **User Service (4ê°œ)**: + - UFR-USER-010: 회ì›ê°€ìž… + - UFR-USER-020: ë¡œê·¸ì¸ + - UFR-USER-030: 프로필 수정 + - UFR-USER-040: 로그아웃 + +- **Event Service (9ê°œ)**: + - UFR-EVT-010: ì´ë²¤íЏ ëª©ì  ì„ íƒ + - UFR-EVT-020: AI ì´ë²¤íЏ 추천 요청 + - UFR-EVT-021: AI 추천 ê²°ê³¼ 조회 + - UFR-EVT-030: ì´ë¯¸ì§€ ìƒì„± 요청 + - UFR-EVT-031: ì´ë¯¸ì§€ ê²°ê³¼ 조회 + - UFR-EVT-040: 콘í…츠 ì„ íƒ + - UFR-EVT-050: 최종 ìŠ¹ì¸ ë° ë°°í¬ + - UFR-EVT-060: ì´ë²¤íЏ ìƒì„¸ 조회 + - UFR-EVT-061: ì´ë²¤íЏ ëª©ë¡ ì¡°íšŒ + +- **Participation Service (3ê°œ)**: + - UFR-PART-010: ì´ë²¤íЏ 참여 + - UFR-PART-011: ì°¸ì—¬ìž ëª©ë¡ ì¡°íšŒ + - UFR-PART-020: ë‹¹ì²¨ìž ì¶”ì²¨ + +- **Analytics Service (2ê°œ)**: + - UFR-ANL-010: 대시보드 조회 + - UFR-ANL-011: 실시간 통계 ì—…ë°ì´íЏ (Kafka 구ë…) + +- **AI/Content/Distribution Service**: 외부 시퀀스ì—서 비ë™ê¸° 처리 + +**design/backend/sequence/outer/사용ìžì¸ì¦í”Œë¡œìš°.puml (249줄)** +- UFR-USER-010: 회ì›ê°€ìž… with 국세청 API Circuit Breaker +- UFR-USER-020: ë¡œê·¸ì¸ with JWT ìƒì„± +- UFR-USER-040: 로그아웃 with 세션 ì‚­ì œ + +**design/backend/sequence/outer/ì´ë²¤íЏìƒì„±í”Œë¡œìš°.puml (211줄)** +- Kafka 기반 비ë™ê¸° 처리 (AI/Image Job) +- Distribution Service ë™ê¸° REST 호출 +- Polling 패턴으로 Job ìƒíƒœ 조회 + +**design/backend/sequence/outer/ê³ ê°ì°¸ì—¬í”Œë¡œìš°.puml (151줄)** +- 참여 ë“±ë¡ with 중복 ì²´í¬ +- ë‹¹ì²¨ìž ì¶”ì²¨ with Fisher-Yates Shuffle +- Kafka ì´ë²¤íЏ 발행 + +**design/backend/sequence/outer/성과분ì„플로우.puml (225줄)** +- Cache HIT/MISS 시나리오 +- 외부 API 병렬 호출 with Circuit Breaker +- Kafka ì´ë²¤íЏ êµ¬ë… + +**design/backend/logical/logical-architecture.md (883줄)** +- Event-Driven 아키í…처 ìƒì„¸ +- Kafka 통합 ì „ëžµ +- Resilience 패턴 구성 + +### ìƒì„±ëœ ë‚´ë¶€ 시퀀스 íŒŒì¼ (26ê°œ) + +#### User Service (4ê°œ 파ì¼) + +**1. design/backend/sequence/inner/user-회ì›ê°€ìž….puml (6.9KB)** +```plantuml +@startuml user-회ì›ê°€ìž… +!theme mono +title User Service - 회ì›ê°€ìž… ë‚´ë¶€ 시퀀스 + +participant "UserController" as Controller +participant "UserService" as Service +participant "BusinessValidator" as Validator +participant "UserRepository" as Repo +participant "Redis Cache<>" as Cache +participant "User DB<>" as DB +participant "국세청 API<>" as NTS + +note over Controller: POST /api/users/register +Controller -> Service: registerUser(RegisterDto) +Service -> Validator: validateBusinessNumber(사업ìžë²ˆí˜¸) + +alt ìºì‹œì— ê²€ì¦ ê²°ê³¼ 존재 (TTL 7ì¼) + Validator -> Cache: GET business:{사업ìžë²ˆí˜¸} + Cache --> Validator: ê²€ì¦ ê²°ê³¼ (HIT) +else ìºì‹œ 미스 + Validator -> NTS: 사업ìžë²ˆí˜¸ ê²€ì¦ API\n[Circuit Breaker, Timeout 5s] + + alt ê²€ì¦ ì„±ê³µ + NTS --> Validator: 유효한 ì‚¬ì—…ìž + Validator -> Cache: SET business:{사업ìžë²ˆí˜¸}\n(TTL 7ì¼) + else Circuit Breaker OPEN (외부 API 장애) + NTS --> Validator: OPEN ìƒíƒœ + note right: Fallback ì „ëžµ:\n- ìºì‹œ ë°ì´í„° 사용\n- ë˜ëŠ” ê²€ì¦ ìŠ¤í‚µ (추후 재검ì¦) + end +end + +Validator --> Service: ê²€ì¦ ì™„ë£Œ + +Service -> Service: 비밀번호 해싱\n(bcrypt, cost factor 10) +Service -> Service: 사업ìžë²ˆí˜¸ 암호화\n(AES-256) + +Service -> Repo: beginTransaction() +Service -> Repo: saveUser(User) +Repo -> DB: INSERT INTO users +DB --> Repo: user_id +Service -> Repo: saveStore(Store) +Repo -> DB: INSERT INTO stores +DB --> Repo: store_id +Service -> Repo: commit() + +Service -> Service: JWT í† í° ìƒì„±\n(만료 7ì¼) +Service -> Cache: SET session:{user_id}\n(JWT, TTL 7ì¼) + +Service --> Controller: RegisterResponseDto\n(user_id, token) +Controller --> Client: 201 Created +@enduml +``` + +**주요 설계 í¬ì¸íЏ**: +- Circuit Breaker: 국세청 API (50% 실패율 → OPEN) +- Cache-Aside: Redis 7ì¼ TTL, 95% 히트율 목표 +- Transaction: User + Store INSERT +- Security: bcrypt 해싱, AES-256 암호화 +- JWT: 7ì¼ ë§Œë£Œ, Redis 세션 저장 + +**2. design/backend/sequence/inner/user-로그ì¸.puml (4.5KB)** +- bcrypt 패스워드 ê²€ì¦ +- JWT í† í° ìƒì„± +- Redis 세션 저장 + +**3. design/backend/sequence/inner/user-프로필수정.puml (6.2KB)** +- User + Store UPDATE 트랜잭션 +- ì„ íƒì  패스워드 변경 ë° ê²€ì¦ + +**4. design/backend/sequence/inner/user-로그아웃.puml (3.6KB)** +- 세션 ì‚­ì œ +- JWT Blacklist (TTL = ë‚¨ì€ ë§Œë£Œ 시간) + +#### Event Service (10ê°œ 파ì¼) + +**1. event-목ì ì„ íƒ.puml** +- ì´ë²¤íЏ ëª©ì  ì„ íƒ +- EventCreated ì´ë²¤íЏ 발행 + +**2. event-AI추천요청.puml** +- Kafka ai-job 토픽 발행 +- Job ID 반환 (202 Accepted) + +**3. event-추천결과조회.puml** +- Redisì—서 Job ìƒíƒœ í´ë§ +- 완료 시 추천 ê²°ê³¼ 반환 + +**4. event-ì´ë¯¸ì§€ìƒì„±ìš”ì²­.puml** +- Kafka image-job 토픽 발행 + +**5. event-ì´ë¯¸ì§€ê²°ê³¼ì¡°íšŒ.puml** +- ìºì‹œì—서 ì´ë¯¸ì§€ URL 조회 + +**6. event-콘í…츠선íƒ.puml** +- 콘í…츠 ì„ íƒ ì €ìž¥ + +**7. event-최종승ì¸ë°ë°°í¬.puml** +- Distribution Service REST API ë™ê¸° 호출 + +**8. event-ìƒì„¸ì¡°íšŒ.puml** +- ì´ë²¤íЏ ìƒì„¸ 조회 with ìºì‹œ (TTL 5ë¶„) + +**9. event-목ë¡ì¡°íšŒ.puml** +- ì´ë²¤íЏ ëª©ë¡ with í•„í„°/검색/페ì´ì§• + +**10. event-대시보드조회.puml** +- 대시보드 ì´ë²¤íЏ with 병렬 쿼리 + +#### Participation Service (3ê°œ 파ì¼) + +**1. participation-ì´ë²¤íŠ¸ì°¸ì—¬.puml (4.6KB)** +```plantuml +@startuml participation-ì´ë²¤íŠ¸ì°¸ì—¬ +!theme mono +title Participation Service - ì´ë²¤íЏ 참여 ë‚´ë¶€ 시퀀스 + +participant "ParticipationController" as Controller +participant "ParticipationService" as Service +participant "ParticipationRepository" as Repo +participant "Redis Cache<>" as Cache +participant "Participation DB<>" as DB +participant "Kafka<>" as Kafka + +note over Controller: POST /api/participations +Controller -> Service: participate(ParticipateDto) + +' 중복 참여 ì²´í¬ +Service -> Cache: EXISTS participation:{event_id}:{user_id} + +alt ìºì‹œì— 중복 ê¸°ë¡ ì¡´ìž¬ + Cache --> Service: true + Service --> Controller: 409 Conflict\n(ì´ë¯¸ 참여함) +else ìºì‹œ 미스 → DB í™•ì¸ + Cache --> Service: false + Service -> Repo: existsByEventAndUser(event_id, user_id) + Repo -> DB: SELECT COUNT(*)\nFROM participations\nWHERE event_id = ? AND user_id = ? + DB --> Repo: count + + alt 중복 참여 발견 + Repo --> Service: true + Service -> Cache: SET participation:{event_id}:{user_id}\n(TTL ì´ë²¤íЏ 종료ì¼) + Service --> Controller: 409 Conflict + else 중복 ì—†ìŒ + Repo --> Service: false + + ' ì‘모번호 ìƒì„± (UUID) + Service -> Service: generateEntryNumber()\n(UUID v4) + + ' 참여 저장 + Service -> Repo: save(Participation) + Repo -> DB: INSERT INTO participations\n(event_id, user_id, entry_number, store_visit) + DB --> Repo: participation_id + + ' ìºì‹œ ì—…ë°ì´íЏ (중복 ì²´í¬ìš©) + Service -> Cache: SET participation:{event_id}:{user_id}\n(TTL ì´ë²¤íЏ 종료ì¼) + Service -> Cache: INCR participant_count:{event_id} + + ' Kafka ì´ë²¤íЏ 발행 + Service -> Kafka: publish('ParticipantRegistered',\n{event_id, user_id, participation_id}) + + Service --> Controller: ParticipateResponseDto\n(participation_id, entry_number) + Controller --> Client: 201 Created + end +end +@enduml +``` + +**주요 설계 í¬ì¸íЏ**: +- 중복 ì²´í¬: Redis Cache + DB +- ParticipantRegistered ì´ë²¤íЏ 발행 +- ì‘모번호 ìƒì„± (UUID) +- ìºì‹œ TTL: ì´ë²¤íЏ 종료ì¼ê¹Œì§€ + +**2. participation-참여ìžëª©ë¡ì¡°íšŒ.puml (4.3KB)** +- ë™ì  쿼리 with í•„í„° +- 전화번호 마스킹 +- ìºì‹œ TTL 5ë¶„ + +**3. participation-당첨ìžì¶”첨.puml (6.5KB)** +```plantuml +' Fisher-Yates Shuffle 알고리즘 +' Crypto.randomBytes로 공정성 보장 +' 매장 방문 보너스 (가중치 x2) +' WinnerSelected ì´ë²¤íЏ 발행 +``` + +#### Analytics Service (5ê°œ 파ì¼) + +**1. analytics-대시보드조회-ìºì‹œížˆíЏ.puml (2.0KB)** +- 0.5ì´ˆ ì‘답 + +**2. analytics-대시보드조회-ìºì‹œë¯¸ìФ.puml (6.4KB)** +```plantuml +par 외부 API 병렬 호출 + Analytics -> WooriAPI: GET ì˜ìƒ ê´‘ê³  통계\n[Circuit Breaker] +and + Analytics -> GenieAPI: GET TV ê´‘ê³  통계\n[Circuit Breaker] +and + Analytics -> SNSAPI: GET SNS ì¸ì‚¬ì´íЏ\n[Circuit Breaker] +end + +' ROI 계산 ë¡œì§ +Analytics -> Analytics: calculateROI()\n(ì´ ìˆ˜ìµ - ì´ ë¹„ìš©) / ì´ ë¹„ìš© × 100 + +' Redis ìºì‹± (TTL 5ë¶„) +Analytics -> Cache: SET dashboard:{event_id}\n(통계 ë°ì´í„°, TTL 5ë¶„) +``` + +**3. analytics-ì´ë²¤íЏìƒì„±êµ¬ë….puml** +- EventCreated → 통계 초기화 + +**4. analytics-참여ìžë“±ë¡êµ¬ë….puml** +- ParticipantRegistered → 실시간 카운트 ì—…ë°ì´íЏ + +**5. analytics-ë°°í¬ì™„료구ë….puml** +- DistributionCompleted → 채ë„별 통계 ì—…ë°ì´íЏ + +#### AI Service (1ê°œ 파ì¼) + +**ai-트렌드분ì„ë°ì¶”천.puml (12KB)** +```plantuml +' Kafka ai-job êµ¬ë… +' 트렌드 ë¶„ì„ ìºì‹œ (TTL 1시간) + +par 3가지 추천 옵션 ìƒì„± + AI -> AIApi: 저비용 옵션 ìƒì„±\n[Circuit Breaker, Timeout 30s] +and + AI -> AIApi: 중간 비용 옵션 ìƒì„±\n[Circuit Breaker, Timeout 30s] +and + AI -> AIApi: 고비용 옵션 ìƒì„±\n[Circuit Breaker, Timeout 30s] +end + +' Circuit Breaker: Claude/GPT-4 API +' Fallback: 기본 템플릿 추천 +' ìºì‹œ ê²°ê³¼ (TTL 24시간) +``` + +#### Content Service (1ê°œ 파ì¼) + +**content-ì´ë¯¸ì§€ìƒì„±.puml (8.5KB)** +```plantuml +' Kafka image-job êµ¬ë… + +par 3가지 ìŠ¤íƒ€ì¼ ë³‘ë ¬ ìƒì„± + Content -> ImageAPI: 심플 스타ì¼\n[Circuit Breaker, Timeout 20s] +and + Content -> ImageAPI: 화려한 스타ì¼\n[Circuit Breaker, Timeout 20s] +and + Content -> ImageAPI: 트렌디 스타ì¼\n[Circuit Breaker, Timeout 20s] +end + +' Fallback: Stable Diffusion → DALL-E → 템플릿 +' CDN 업로드 ë° URL ìºì‹± (TTL 7ì¼) +``` + +#### Distribution Service (2ê°œ 파ì¼) + +**1. distribution-다중채ë„ë°°í¬.puml (11KB)** +```plantuml +' REST API ë™ê¸° 호출 (Event Service로부터) + +par 다중 ì±„ë„ ë°°í¬ (Bulkhead) + Dist -> WooriAPI: ì˜ìƒ ê´‘ê³  업로드\n[Retry 3회, Timeout 10s] +and + Dist -> LingoAPI: 벨소리/ì»¬ëŸ¬ë§ ì—…ë°ì´íЏ\n[Retry 3회, Timeout 10s] +and + Dist -> GenieAPI: TV ê´‘ê³  등ë¡\n[Retry 3회, Timeout 10s] +and + Dist -> InstagramAPI: 피드 게시\n[Retry 3회, Timeout 10s] +and + Dist -> NaverAPI: 블로그 í¬ìŠ¤íŒ…\n[Retry 3회, Timeout 10s] +and + Dist -> KakaoAPI: ì±„ë„ ê²Œì‹œ\n[Retry 3회, Timeout 10s] +end + +' DistributionCompleted ì´ë²¤íЏ 발행 +' ë…립 ì±„ë„ ì²˜ë¦¬ (한 ì±„ë„ ì‹¤íŒ¨í•´ë„ ë‹¤ë¥¸ ì±„ë„ ê³„ì†) +``` + +**2. distribution-ë°°í¬ìƒíƒœì¡°íšŒ.puml (6.5KB)** +- ë°°í¬ ìƒíƒœ ëª¨ë‹ˆí„°ë§ with Retry 기능 + +### 요약 문서 + +**design/backend/sequence/inner/README.md** +- **ì´ í¬ê¸°**: 115.6KB (모든 íŒŒì¼ í•©ê³„) +- **ë‚´ìš©**: + - 26ê°œ 시나리오 완전 문서화 + - 설계 ì›ì¹™ ë° íŒ¨í„´ 설명 + - íŒŒì¼ êµ¬ì¡° ë° í†µê³„ + - PlantUML ê²€ì¦ ë°©ë²• (온ë¼ì¸/로컬/IDE) + +**íŒŒì¼ í†µê³„**: +- ì´ 26ê°œ íŒŒì¼ +- 7ê°œ 서비스 +- í‰ê·  íŒŒì¼ í¬ê¸°: 4.4KB +- 최소: 1.6KB (ìºì‹œ 히트 시나리오) +- 최대: 12KB (AI 트렌드 ë¶„ì„) + +--- + +## 4. 오류 ë° ìˆ˜ì • (Errors and Fixes) + +### 오류 1: Docker 미실행 + +**설명**: PlantUML 문법 검사는 Docker ë°ëª¬ì´ 필요하지만 실행 ì¤‘ì´ ì•„ë‹˜ + +**시ë„한 명령어**: +```bash +cat "file.puml" | docker exec -i plantuml java -jar /app/plantuml.jar -syntax +``` + +**오류 메시지**: +``` +Cannot connect to the Docker daemon at unix:///var/run/docker.sock. +Is the docker daemon running? +``` + +**í•´ê²° 방법**: +- ìˆ˜ë™ ë¬¸ë²• ê²€ì¦ ìˆ˜í–‰ +- 모든 서브 ì—ì´ì „트 ë³´ê³ : 문법 ìˆ˜ë™ ê²€ì¦ ì™„ë£Œ, 유효하지 ì•Šì€ í™”ì‚´í‘œ 문법(`..>`) 사용 안함, ì ì ˆí•œ 구조 í™•ì¸ + +**ì‚¬ìš©ìž í”¼ë“œë°±**: ì—†ìŒ - 사용ìžê°€ ì´ ì œí•œ 사항 수용 + +### 오류 2: ì—†ìŒ - 모든 작업 성공ì ìœ¼ë¡œ 완료 + +- 26ê°œ PlantUML íŒŒì¼ ëª¨ë‘ ë¬¸ë²• 오류 ì—†ì´ ìƒì„± +- 모든 íŒŒì¼ `!theme mono` 표준 준수 +- ì ì ˆí•œ participant ì„ ì–¸ ë° í™”ì‚´í‘œ 문법 + +--- + +## 5. 문제 í•´ê²° (Problem Solving) + +### 문제 1: 7ê°œ 서비스 병렬 처리 + +**í•´ê²°ì±…**: Task ë„구로 7ê°œ ë…립 서브 ì—ì´ì „트 ìƒì„± +- ê° ì„œë¸Œ ì—ì´ì „트는 system-architect 타입 +- ë™ì¼í•œ 지침 ë° ì°¸ì¡° 문서 제공 + +**ì´ì **: +- 모든 서비스 ë™ì‹œ 설계 +- ì´ ì†Œìš” 시간 단축 + +**ê²°ê³¼**: 26ê°œ íŒŒì¼ ë³‘ë ¬ ìƒì„± + +### 문제 2: 서비스 ê°„ ì¼ê´€ì„± 보장 + +**í•´ê²°ì±…**: ê° ì„œë¸Œ ì—ì´ì „íŠ¸ì— ë™ì¼í•œ 지침 제공 +- 공통 설계 ì›ì¹™ +- ë‚´ë¶€ 시퀀스 설계 ê°€ì´ë“œ +- 외부 시퀀스 다ì´ì–´ê·¸ëž¨ +- 논리 아키í…처 + +**ê²°ê³¼**: ì¼ê´€ëœ ë ˆì´ì–´ë§, 네ì´ë°, 패턴 ì ìš© + +### 문제 3: 복잡한 ì‹œë‚˜ë¦¬ì˜¤ì˜ ë‹¤ì¤‘ 패턴 ì ìš© + +**예시**: Distribution Service 다중 ì±„ë„ ë°°í¬ + +**í•´ê²°ì±…**: PlantUML `par/and/end` ë¸”ë¡ ì‚¬ìš© +```plantuml +par 다중 ì±„ë„ ë°°í¬ + Dist -> WooriAPI: [Circuit Breaker, Retry, Timeout] +and + Dist -> LingoAPI: [Circuit Breaker, Retry, Timeout] +and + Dist -> GenieAPI: [Circuit Breaker, Retry, Timeout] +and + Dist -> InstagramAPI: [Circuit Breaker, Retry, Timeout] +and + Dist -> NaverAPI: [Circuit Breaker, Retry, Timeout] +and + Dist -> KakaoAPI: [Circuit Breaker, Retry, Timeout] +end +``` + +**ì ìš© 패턴**: +- Circuit Breaker (채ë„별) +- Bulkhead (격리) +- Retry (지수 백오프) + +### 문제 4: ìºì‹œ ì „ëžµ ì •ì˜ + +**í•´ê²°ì±…**: ë°ì´í„° ë³€ë™ì„±ì— 따른 TTL 차등 ì ìš© + +| ë°ì´í„° 유형 | TTL | ì´ìœ  | +|------------|-----|------| +| ì •ì  ë°ì´í„° (사업ìžë²ˆí˜¸ ê²€ì¦) | 7ì¼ | ê±°ì˜ ë³€ê²½ ì—†ìŒ | +| ëŠë¦¬ê²Œ 변경 (AI 추천 ê²°ê³¼) | 24시간 | ì¼ë³„ 트렌드 ë°˜ì˜ | +| 실시간 (대시보드) | 5ë¶„ | 최신 통계 í•„ìš” | +| 빈번 ì—…ë°ì´íЏ (ì´ë²¤íЏ 목ë¡) | 1ë¶„ | 실시간성 중요 | + +--- + +## 6. 모든 ì‚¬ìš©ìž ë©”ì‹œì§€ (All User Messages) + +### 메시지 1: 초기 명령 + +``` +/design-seq-inner @architecture +ë‚´ë¶€ 시퀀스 설계를 í•´ 주세요: +- '공통설계ì›ì¹™'ê³¼ '내부시퀀스설계 ê°€ì´ë“œ'를 준용하여 설계 +``` + +### 메시지 2: 요약 요청 + +``` +Your task is to create a detailed summary of the conversation so far, +with the following structure: + +1. **Primary Request and Intent**: + Identify the main user request(s) and underlying goals/expectations. + +2. **Key Technical Concepts**: + List and briefly explain all technical concepts, patterns, architectures, + or methodologies discussed (e.g., patterns like Circuit Breaker, CQRS, etc.) + +3. **Files and Code Sections**: + Enumerate all files referenced or created, with brief descriptions. + Include key code snippets with context. + +4. **Errors and Fixes**: + Detail any errors encountered, their causes, and how they were resolved. + +5. **Problem Solving**: + Outline problems that arose and the strategies used to overcome them. + +6. **All User Messages**: + Reproduce every message the user sent, in chronological order. + +7. **Pending Tasks**: + List any tasks or questions that remain unresolved or incomplete. + +8. **Current Work**: + Describe what actions were being taken immediately before the summary was requested. + +9. **Optional Next Step**: + Suggest a logical next step, but do not proceed—only recommend. + +Please be thorough and provide sufficient detail so that I can fully +understand the conversation without needing to review the entire chat history. +``` + +--- + +## 7. 보류 ì¤‘ì¸ ìž‘ì—… (Pending Tasks) + +**보류 작업 ì—†ìŒ** - 11ê°œ TODO 항목 ëª¨ë‘ ì™„ë£Œ: + +- ✅ ê°€ì´ë“œ 문서 ë¶„ì„ ë° í”„ë¡œì íЏ ìƒí™© 파악 +- ✅ 외부 시퀀스 ë° ë…¼ë¦¬ 아키í…처 ë¶„ì„ +- ✅ 서비스별 시나리오 분류 ë° ì„¤ê³„ ê³„íš ìˆ˜ë¦½ +- ✅ User 서비스 ë‚´ë¶€ 시퀀스 설계 +- ✅ Event 서비스 ë‚´ë¶€ 시퀀스 설계 +- ✅ AI 서비스 ë‚´ë¶€ 시퀀스 설계 +- ✅ Content 서비스 ë‚´ë¶€ 시퀀스 설계 +- ✅ Distribution 서비스 ë‚´ë¶€ 시퀀스 설계 +- ✅ Participation 서비스 ë‚´ë¶€ 시퀀스 설계 +- ✅ Analytics 서비스 ë‚´ë¶€ 시퀀스 설계 +- ✅ ì „ì²´ ë‚´ë¶€ 시퀀스 설계 ê²€ì¦ ë° í†µí•© + +--- + +## 8. 현재 작업 (Current Work) + +요약 요청 ì§ì „ì— ìµœì¢… 통합 단계를 완료했습니다: + +### 작업 1: íŒŒì¼ ìƒì„± ê²€ì¦ + +```bash +find design/backend/sequence/inner -name "*.puml" -type f | wc -l +# ê²°ê³¼: 26ê°œ íŒŒì¼ + +ls -lh design/backend/sequence/inner/*.puml | awk '{print $9, $5}' | sort +# 26ê°œ íŒŒì¼ ëª©ë¡ ë° í¬ê¸° (1.6KB ~ 12KB) +``` + +### 작업 2: 종합 README ìƒì„± + +**파ì¼**: `design/backend/sequence/inner/README.md` +**í¬ê¸°**: 115.6KB ì´í•©ì˜ ì¼ë¶€ +**ë‚´ìš©**: +- 서비스-시나리오별 26ê°œ 분류 +- 설계 ì›ì¹™ 문서화 +- 패턴 설명 (Resilience, Cache-Aside, Event-Driven) +- ìºì‹œ ì „ëžµ 표 (TTL, 히트율 목표) +- íŒŒì¼ êµ¬ì¡° ëª©ë¡ +- PlantUML ê²€ì¦ ë°©ë²• (온ë¼ì¸/로컬/IDE) +- 통계: 7ê°œ 서비스, 26ê°œ 파ì¼, í‰ê·  4.4KB + +### 작업 3: TODO ëª©ë¡ ì—…ë°ì´íЏ + +```javascript +TodoWrite: 11ê°œ 작업 ëª¨ë‘ "completed" ìƒíƒœ +``` + +### 최종 ìƒíƒœ + +- 모든 ë‚´ë¶€ 시퀀스 설계 완료 +- 모든 문서 통합 완료 +- ë‹¤ìŒ ë‹¨ê³„ 준비 완료 (API 설계 ë˜ëŠ” í´ëž˜ìФ 설계) + +--- + +## 9. ì„ íƒì  ë‹¤ìŒ ë‹¨ê³„ (Optional Next Step) + +**ë‹¤ìŒ ë‹¨ê³„ 권장 ì—†ìŒ** - ìž‘ì—…ì´ ì™„ì „ížˆ 완료ë˜ì—ˆìŠµë‹ˆë‹¤. + +사용ìžê°€ 명시ì ìœ¼ë¡œ 요청한 "ë‚´ë¶€ 시퀀스 설계를 í•´ 주세요"는 다ìŒì„ 통해 완수ë˜ì—ˆìŠµë‹ˆë‹¤: + +- ✅ 26ê°œ PlantUML ë‚´ë¶€ 시퀀스 다ì´ì–´ê·¸ëž¨ +- ✅ 종합 README 문서화 +- ✅ 모든 설계 ì›ì¹™ ë° íŒ¨í„´ ì ìš© +- ✅ 외부 시퀀스 ë° ì•„í‚¤í…처와 완전 ì •ë ¬ + +### 아키í…처 설계 워í¬í”Œë¡œìš°ì˜ ì¼ë°˜ì ì¸ ë‹¤ìŒ ë‹¨ê³„: + +사용ìžê°€ ê³„ì† ì§„í–‰í•˜ê³ ìž í•œë‹¤ë©´: + +1. **API 설계** (`/design-api`): + - OpenAPI 3.0 명세 ì •ì˜ + - 26ê°œ ì‹œë‚˜ë¦¬ì˜¤ì˜ ëª¨ë“  엔드í¬ì¸íЏ 명세화 + - Request/Response DTO ì •ì˜ + - 보안 스키마 (JWT) + - ì—러 코드 체계 + +2. **í´ëž˜ìФ 설계** (`/design-class`): + - í´ëž˜ìФ 다ì´ì–´ê·¸ëž¨ ìƒì„± + - Clean/Hexagonal 아키í…처 ì ìš© + - 패키지 구조 ì •ì˜ + - ì¸í„°íŽ˜ì´ìФ ë° êµ¬í˜„ í´ëž˜ìФ 분리 + +3. **ë°ì´í„° 설계** (`/design-data`): + - ë°ì´í„°ë² ì´ìФ 스키마 설계 + - ERD (Entity-Relationship Diagram) + - ì¸ë±ìФ ì „ëžµ + - 샤딩/íŒŒí‹°ì…”ë‹ ì „ëžµ + +그러나 **명시ì ì¸ ì‚¬ìš©ìž í™•ì¸ì„ 기다려야 합니다**. 현재 ìž‘ì—…ì´ ì™„ë£Œë˜ì—ˆê³  사용ìžê°€ 다른 우선순위를 가질 수 있기 때문입니다. + +--- + +## 📊 프로ì íЏ 통계 요약 + +### 산출물 +- **PlantUML 파ì¼**: 26ê°œ +- **ì´ í¬ê¸°**: 115.6KB +- **서비스**: 7ê°œ +- **시나리오**: 26ê°œ + +### 서비스별 íŒŒì¼ ìˆ˜ +- User Service: 4ê°œ +- Event Service: 10ê°œ +- Participation Service: 3ê°œ +- Analytics Service: 5ê°œ +- AI Service: 1ê°œ +- Content Service: 1ê°œ +- Distribution Service: 2ê°œ + +### 패턴 ì ìš© 현황 +- Circuit Breaker: 12ê°œ 시나리오 +- Retry Pattern: 8ê°œ 시나리오 +- Timeout Pattern: 15ê°œ 시나리오 +- Fallback Pattern: 10ê°œ 시나리오 +- Bulkhead Pattern: 1ê°œ 시나리오 (Distribution) +- Cache-Aside: 20ê°œ 시나리오 +- Event-Driven: 7ê°œ Kafka ì´ë²¤íЏ/Job + +### 작업 수행 ë°©ì‹ +- **병렬 처리**: 7ê°œ 서브 ì—ì´ì „트 ë™ì‹œ 실행 +- **설계 표준 준수**: 공통설계ì›ì¹™, 내부시퀀스설계 ê°€ì´ë“œ +- **ê²€ì¦ ë°©ë²•**: ìˆ˜ë™ PlantUML 문법 ê²€ì¦ (Docker 미사용) + +--- + +## ✅ 완료 í™•ì¸ + +ì´ ìš”ì•½ 문서는 다ìŒì„ í¬í•¨í•©ë‹ˆë‹¤: + +1. ✅ 주요 요청 ë° ì˜ë„ +2. ✅ 주요 기술 ê°œë… (아키í…처, 패턴, 보안, ìºì‹±) +3. ✅ íŒŒì¼ ë° ì½”ë“œ 섹션 (ê°€ì´ë“œ, 참조 문서, ìƒì„± 파ì¼) +4. ✅ 오류 ë° ìˆ˜ì • (Docker 미실행 → ìˆ˜ë™ ê²€ì¦) +5. ✅ 문제 í•´ê²° (병렬 처리, ì¼ê´€ì„±, 복잡한 패턴, ìºì‹œ ì „ëžµ) +6. ✅ 모든 ì‚¬ìš©ìž ë©”ì‹œì§€ (초기 명령, 요약 요청) +7. ✅ 보류 ì¤‘ì¸ ìž‘ì—… (ì—†ìŒ - ëª¨ë‘ ì™„ë£Œ) +8. ✅ 현재 작업 (최종 통합 ë° ê²€ì¦) +9. ✅ ì„ íƒì  ë‹¤ìŒ ë‹¨ê³„ (API/í´ëž˜ìФ/ë°ì´í„° 설계 권장) + +**문서 작성ì¼**: 2025ë…„ +**작성ìž**: Claude Code (Sonnet 4.5) +**프로ì íЏ**: KT AI 기반 소ìƒê³µì¸ ì´ë²¤íЏ ìžë™ ìƒì„± 서비스 diff --git a/claude/logical-architecture-design.md b/claude/logical-architecture-design.md new file mode 100644 index 0000000..ca4d2ba --- /dev/null +++ b/claude/logical-architecture-design.md @@ -0,0 +1,64 @@ +# 논리아키í…처설계가ì´ë“œ + +[요청사항] +- <작성ì›ì¹™>ì„ ì¤€ìš©í•˜ì—¬ 설계 +- <작성순서>ì— ë”°ë¼ ì„¤ê³„ +- [결과파ì¼] ì•ˆë‚´ì— ë”°ë¼ íŒŒì¼ ìž‘ì„± +- 완료 후 mermaid 스í¬ë¦½íЏ 테스트 방법 안내 + - https://mermaid.live/edit ì— ì ‘ê·¼ + - 스í¬ë¦½íЏ ë‚´ìš©ì„ ë¶™ì—¬ë„£ì–´ í™•ì¸ + +[ê°€ì´ë“œ] +<작성ì›ì¹™> +- **유저스토리와 매칭**ë˜ì–´ì•¼ 함. **불필요한 추가 설계 금지** +- UI/UXì„¤ê³„ì„œì˜ 'ì‚¬ìš©ìž í”Œë¡œìš°'참조하여 설계 +- '아키í…처패턴'ì— ì„ ì •ëœ í´ë¼ìš°ë“œ ë””ìžì¸ íŒ¨í„´ì„ ì ìš©í•˜ì—¬ 설계 +- ì‚¬ìš©ìž ê´€ì ì˜ ì»´í¬ë„ŒíЏ 다ì´ì–´ê·¸ëž¨ 작성 +- Context Map 스타ì¼ë¡œ 서비스 ë‚´ë¶€ 구조는 ìƒëžµí•˜ê³  서비스 ê°„ ê´€ê³„ì— ì§‘ì¤‘ +- í´ë¼ì´ì–¸íЏì—서 API Gateway로는 ë‹¨ì¼ ì—°ê²°ì„ ìœ¼ë¡œ 표현 +<작성순서> +- 준비: + - 유저스토리, UI/UX설계서, 아키í…처패턴 ë¶„ì„ ë° ì´í•´ + - "@analyze --play" í”„ë¡œí† íƒ€ìž…ì´ ìžˆëŠ” 경우 웹브ë¼ìš°ì €ì—서 실행하여 서비스 ì´í•´ +- 실행: + - 논리아키í…처 설계서(logical-architecture.md) 작성: 아래 í•­ëª©ì€ í•„ìˆ˜ í¬í•¨í•˜ê³  í•„ìš” 시 항목 추가 + - 개요: 설계 ì›ì¹™, 핵심 ì»´í¬ë„ŒíЏ ì •ì˜ + - 서비스 아키í…처 + - 서비스별 ì±…ìž„ + - 서비스 ê°„ 통신 ì „ëžµ + - 주요 ì‚¬ìš©ìž í”Œë¡œìš° + - ë°ì´í„° í름 ë° ìºì‹± ì „ëžµ + - 확장성 ë° ì„±ëŠ¥ 고려사항 + - 보안 고려사항 + - 논리아키í…처 다ì´ì–´ê·¸ëž¨ + - Mermaid 형ì‹ìœ¼ë¡œ 작성하며 ë³„ë„ íŒŒì¼ë¡œ 작성: logical-architecture.mmd + - <통신전략>ê³¼ <ì˜ì¡´ì„± 표현 방법>ì„ ì¤€ìˆ˜ + - **Mermaid 스í¬ë¦½íЏ íŒŒì¼ ê²€ì‚¬ 실행**: 'Mermaid문법검사가ì´ë“œ' 준용 +- 검토: + - <작성ì›ì¹™> 준수 검토 + - 스쿼드 íŒ€ì› ë¦¬ë·°: ëˆ„ë½ ë° ê°œì„  사항 검토 + - 수정 사항 ì„ íƒ ë° ë°˜ì˜ +<통신 ì „ëžµ> +- **ë™ê¸° 통신**: 즉시 ì‘ë‹µì´ í•„ìš”í•œ 단순 조회 +- **ìºì‹œ ìš°ì„ **: ìžì£¼ 사용ë˜ëŠ” ë°ì´í„°ëŠ” ìºì‹œì—서 ì§ì ‘ ì½ê¸° +- **비ë™ê¸° 처리**: 외부 API 다중 호출 등 장시간 작업 +<ì˜ì¡´ì„± 표현 방법> +- 실선 화살표(→): ë™ê¸°ì  ì˜ì¡´ì„± (필수) +- 비ë™ê¸° 화살표(->>): 비ë™ê¸° ì˜ì¡´ì„± (fire-and-forget) +- ì ì„  화살표(-->): ì„ íƒì  ì˜ì¡´ì„± ë˜ëŠ” ëŠìŠ¨í•œ ê²°í•© +- ì–‘ë°©í–¥ 화살표(↔): ìƒí˜¸ ì˜ì¡´ì„± +- ì˜ì¡´ì„± ë ˆì´ë¸”ì— ëª©ì  ëª…ì‹œ (예: "멤버 ì •ë³´ 조회") +- 플로우 ë¼ë²¨ 형ì‹: [요청서비스약어]ì•¡ì…˜ (예: [Trip]AI ì¼ì • ìƒì„± 요청) + +[참고ìžë£Œ] +- 유저스토리 +- UI/UX설계서 +- 프로토타입 +- 아키í…처패턴 + +[예시] +- 논리아키í…처 다ì´ì–´ê·¸ëž¨: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/samples/sample-논리아키í…처.mmd + +[결과파ì¼] +- design/backend/logical/logical-architecture.md +- design/backend/logical/logical-architecture.mmd diff --git a/claude/mermaid-guide.md b/claude/mermaid-guide.md new file mode 100644 index 0000000..6d6b3f0 --- /dev/null +++ b/claude/mermaid-guide.md @@ -0,0 +1,300 @@ +# Mermaid문법검사가ì´ë“œ + +## 개요 + +Mermaid 다ì´ì–´ê·¸ëž¨ì˜ 문법 오류를 ì‚¬ì „ì— ê²€ì¶œí•˜ì—¬ ë Œë”ë§ ì‹¤íŒ¨ë¥¼ 방지하기 위한 ê°€ì´ë“œìž…니다. Docker 기반 Mermaid CLI를 활용하여 로컬ì—서 빠르게 ë¬¸ë²•ì„ ê²€ì¦í•  수 있습니다. + +## Mermaid CLI 서버 설치 ë° ê²€ì‚¬ + +### Docker로 Mermaid CLI 컨테ì´ë„ˆ 실행 + +```bash +# Mermaid CLI 컨테ì´ë„ˆê°€ 실행 중ì¸ì§€ í™•ì¸ +docker ps | grep mermaid-cli + +# âš ï¸ ì¤‘ìš”: 첫 실행 시 ì´ë¯¸ì§€ 다운로드를 먼저 ì§„í–‰ (í° ì´ë¯¸ì§€ë¡œ ì¸í•œ 타임아웃 ë°©ì§€) +docker pull minlag/mermaid-cli:latest + +# Mermaid CLI 컨테ì´ë„ˆê°€ 없으면 설치 ë° ì‹¤í–‰ (root 권한으로 실행, í¬íЏ 48080 사용) +docker run -d --rm --name mermaid-cli -u root -p 48080:8080 --entrypoint sh minlag/mermaid-cli:latest -c "while true;do sleep 3600; done" + +# 컨테ì´ë„ˆ ìƒíƒœ í™•ì¸ +docker logs mermaid-cli +``` + +#### 💡 Docker ì´ë¯¸ì§€ 다운로드 관련 주ì˜ì‚¬í•­ + +**첫 실행 시 ë°œìƒí•  수 있는 문제:** +- `minlag/mermaid-cli:latest` ì´ë¯¸ì§€ê°€ í° ìš©ëŸ‰(약 700MB+)ì´ë¯€ë¡œ ë‹¤ìš´ë¡œë“œì— ì‹œê°„ì´ ì˜¤ëž˜ 걸림 +- `docker run` 명령 실행 시 ì´ë¯¸ì§€ê°€ 없으면 ìžë™ 다운로드하지만 타임아웃 ë°œìƒ ê°€ëŠ¥ +- **해결방법**: `docker pull` 명령으로 ì´ë¯¸ì§€ë¥¼ 먼저 다운로드한 후 컨테ì´ë„ˆ 실행 + +**권장 실행 순서:** +1. `docker pull minlag/mermaid-cli:latest` (ì´ë¯¸ì§€ 다운로드) +2. `docker run` 명령으로 컨테ì´ë„ˆ 실행 +3. 필수 설정 ì§„í–‰ + +#### âš ï¸ ì¤‘ìš”: 최초 컨테ì´ë„ˆ ìƒì„± 후 필수 설정 + +Mermaid CLI는 Puppeteer를 사용하여 다ì´ì–´ê·¸ëž¨ì„ ë Œë”ë§í•˜ë¯€ë¡œ Chromium 브ë¼ìš°ì €ê°€ 필요합니다. +컨테ì´ë„ˆë¥¼ ì²˜ìŒ ìƒì„±í•œ 후 ë‹¤ìŒ ëª…ë ¹ì„ ì‹¤í–‰í•˜ì—¬ 필요한 패키지를 설치해야 합니다: + +```bash +# Chromium ë° í•„ìš”í•œ 종ì†ì„± 설치 +docker exec mermaid-cli sh -c "apk add --no-cache chromium chromium-chromedriver nss freetype harfbuzz ca-certificates ttf-freefont" + +# Puppeteerê°€ 사용할 설정 íŒŒì¼ ìƒì„± +docker exec mermaid-cli sh -c "echo '{\"executablePath\": \"/usr/bin/chromium-browser\", \"args\": [\"--no-sandbox\", \"--disable-setuid-sandbox\", \"--disable-dev-shm-usage\"]}' > /tmp/puppeteer-config.json" +``` + +ì´ ì„¤ì •ì€ ì»¨í…Œì´ë„ˆê°€ 실행ë˜ëŠ” ë™ì•ˆ 유지ë˜ë¯€ë¡œ 한 번만 실행하면 ë©ë‹ˆë‹¤. + +문법검사 후 Container를 중지하지 않고 ê³„ì† ì‚¬ìš©í•¨ + +## 문법 검사 방법 +현재 OSì— ë§žê²Œ 수행. + +### Linux/macOS 버전 +**스í¬ë¦½íЏ 파ì¼(tools/check-mermaid.sh)ì„ ì´ìš©í•˜ì—¬ 수행** + +1. tools/check-mermaid.sh íŒŒì¼ ì¡´ìž¬ 여부 í™•ì¸ +2. 스í¬ë¦½íЏ 파ì¼ì´ 없으면 "Mermaid문법검사기(Linux/Mac)"를 tools/check-mermaid.sh 파ì¼ë¡œ 다운로드하여 스í¬ë¦½íЏ 파ì¼ì„ 만듦 +3. 스í¬ë¦½íЏ 파ì¼ì´ 있으면 ê·¸ 스í¬ë¦½íЏ 파ì¼ì„ ì´ìš©í•˜ì—¬ 검사 + +```bash +# 1. 스í¬ë¦½íЏ 실행 권한 부여 (최초 1회) +chmod +x tools/check-mermaid.sh + +# 2. 문법 검사 실행 +./tools/check-mermaid.sh {검사할 파ì¼} + +# 예시 +./tools/check-mermaid.sh design/backend/physical/physical-architecture.mmd +``` + +### Windows PowerShell 버전 +**스í¬ë¦½íЏ 파ì¼(tools/check-mermaid.ps1)ì„ ì´ìš©í•˜ì—¬ 수행** + +1. tools/check-mermaid.ps1 íŒŒì¼ ì¡´ìž¬ 여부 í™•ì¸ +2. 스í¬ë¦½íЏ 파ì¼ì´ 없으면 "Mermaid문법검사기(Window)"를 tools/check-mermaid.ps1 파ì¼ë¡œ 다운로드하여 스í¬ë¦½íЏ 파ì¼ì„ 만듦 +3. 스í¬ë¦½íЏ 파ì¼ì´ 있으면 ê·¸ 스í¬ë¦½íЏ 파ì¼ì„ ì´ìš©í•˜ì—¬ 검사 + +```powershell +# 문법 검사 실행 +.\tools\check-mermaid.ps1 {검사할 파ì¼} + +# 예시 +.\tools\check-mermaid.ps1 design\backend\physical\physical-architecture.mmd +``` + +### ìˆ˜ë™ ê²€ì‚¬ 방법 (스í¬ë¦½íЏ ì—†ì´) + +```bash +# 1. 고유 파ì¼ëª… ìƒì„± (ì¶©ëŒ ë°©ì§€) +TEMP_FILE="/tmp/mermaid_$(date +%s)_$$.mmd" + +# 2. íŒŒì¼ ë³µì‚¬ +docker cp {검사할 파ì¼} mermaid-cli:${TEMP_FILE} + +# 3. 문법 검사 (Puppeteer 설정 íŒŒì¼ ì‚¬ìš©) +docker exec mermaid-cli sh -c "cd /home/mermaidcli && node_modules/.bin/mmdc -i ${TEMP_FILE} -o /tmp/output.svg -p /tmp/puppeteer-config.json -q" + +# 4. 임시 íŒŒì¼ ì‚­ì œ +docker exec mermaid-cli rm -f ${TEMP_FILE} /tmp/output.svg +``` + +**주ì˜**: Puppeteer 설정 파ì¼(`/tmp/puppeteer-config.json`)ì´ ìžˆì–´ì•¼ 합니다. 없다면 ìœ„ì˜ "최초 컨테ì´ë„ˆ ìƒì„± 후 필수 설정"ì„ ë¨¼ì € 실행하세요. + +### 검사 ê²°ê³¼ í•´ì„ + +| 출력 | ì˜ë¯¸ | ëŒ€ì‘ ë°©ë²• | +|------|------|-----------| +| "Success: Mermaid syntax is valid!" | 문법 오류 ì—†ìŒ âœ… | ì •ìƒ, ë Œë”ë§ ê°€ëŠ¥ | +| "Parse error on line X" | X번째 줄 구문 오류 ⌠| 해당 ë¼ì¸ 문법 í™•ì¸ | +| "Expecting 'XXX'" | ì˜ˆìƒ í† í° ì˜¤ë¥˜ ⌠| 누ë½ëœ 문법 요소 추가 | +| "Syntax error" | ì¼ë°˜ 문법 오류 ⌠| ì „ì²´ 구조 재검토 | + +## Mermaid 다ì´ì–´ê·¸ëž¨ 타입별 주ì˜ì‚¬í•­ + +### 1. Graph/Flowchart +```mermaid +graph TB + %% 올바른 사용법 ✅ + A[Node A] --> B[Node B] + C[Node C] -.-> D[Node D] + E[Node E] ==> F[Node F] + + %% 주ì˜ì‚¬í•­ + %% - 노드 IDì— ê³µë°± 불가 (대신 [Label] 사용) + %% - subgraph와 end 개수 ì¼ì¹˜ í•„ìš” + %% - 따옴표 안ì—서 íŠ¹ìˆ˜ë¬¸ìž ì£¼ì˜ +``` + +### 2. Sequence Diagram +```mermaid +sequenceDiagram + %% 올바른 사용법 ✅ + participant A as Service A + participant B as Service B + + A->>B: Request + B-->>A: Response + A->>+B: Call with activation + B-->>-A: Return with deactivation + + %% 주ì˜ì‚¬í•­ + %% - participant ì„ ì–¸ 권장 + %% - 활성화(+)/비활성화(-) ìŒ ë§žì¶”ê¸° + %% - Note ë¸”ë¡ ì¢…ë£Œ í™•ì¸ +``` + +### 3. Class Diagram +```mermaid +classDiagram + %% 올바른 사용법 ✅ + class Animal { + +String name + +int age + +makeSound() void + } + + class Dog { + +String breed + +bark() void + } + + Animal <|-- Dog : inherits + + %% 주ì˜ì‚¬í•­ + %% - 메서드 괄호 필수 + %% - 관계 표현 정확히 + %% - ì ‘ê·¼ ì œí•œìž ê¸°í˜¸ í™•ì¸ +``` + +### 4. State Diagram +```mermaid +stateDiagram-v2 + %% 올바른 사용법 ✅ + [*] --> Idle + Idle --> Processing : start + Processing --> Completed : finish + Processing --> Error : error + Error --> Idle : reset + Completed --> [*] + + %% 주ì˜ì‚¬í•­ + %% - [*]는 시작/종료 ìƒíƒœ + %% - ìƒíƒœ ì´ë¦„ì— ê³µë°± 불가 + %% - ì „ì´ ë ˆì´ë¸” 콜론(:) 사용 +``` + +## ì¼ë°˜ì ì¸ 오류와 í•´ê²° 방법 + +### 1. 괄호 불균형 +```mermaid +%% ìž˜ëª»ëœ ì˜ˆ ⌠+graph TB + A[Node (with parenthesis)] %% 괄호 ì•ˆì— ê´„í˜¸ + +%% 올바른 예 ✅ +graph TB + A[Node with parenthesis] +``` + +### 2. 특수 ë¬¸ìž ì´ìŠ¤ì¼€ì´í”„ +```mermaid +%% ìž˜ëª»ëœ ì˜ˆ ⌠+graph TB + A[Security & Management] %% & ë¬¸ìž ì§ì ‘ 사용 + +%% 올바른 예 ✅ +graph TB + A[Security & Management] %% HTML 엔티티 사용 +``` + +### 3. subgraph/end 불ì¼ì¹˜ +```mermaid +%% ìž˜ëª»ëœ ì˜ˆ ⌠+graph TB + subgraph One + A --> B + subgraph Two + C --> D + end %% Twoë§Œ 닫힘, Oneì€ ì•ˆ 닫힘 + +%% 올바른 예 ✅ +graph TB + subgraph One + A --> B + subgraph Two + C --> D + end + end %% 모든 subgraph 닫기 +``` + +### 4. 노드 참조 오류 +```mermaid +%% ìž˜ëª»ëœ ì˜ˆ ⌠+graph TB + A --> UnknownNode %% ì •ì˜ë˜ì§€ ì•Šì€ ë…¸ë“œ + +%% 올바른 예 ✅ +graph TB + A[Node A] --> B[Node B] %% 모든 노드 ì •ì˜ +``` + +## 컨테ì´ë„ˆ 관리 + +### 컨테ì´ë„ˆ 중지 ë° ì‚­ì œ +```bash +# 컨테ì´ë„ˆ 중지 +docker stop mermaid-cli + +# 컨테ì´ë„ˆ ì‚­ì œ +docker rm mermaid-cli + +# 한 ë²ˆì— ì¤‘ì§€ ë° ì‚­ì œ +docker stop mermaid-cli && docker rm mermaid-cli +``` + +### 컨테ì´ë„ˆ 재시작 +```bash +# 컨테ì´ë„ˆ 재시작 +docker restart mermaid-cli +``` + +## 성능 최ì í™” íŒ + +1. **컨테ì´ë„ˆ 유지**: 검사 후 컨테ì´ë„ˆë¥¼ 중지하지 않고 유지하여 ë‹¤ìŒ ê²€ì‚¬ 시 빠르게 실행 +2. **배치 검사**: 여러 파ì¼ì„ ì—°ì†ìœ¼ë¡œ 검사할 때 컨테ì´ë„ˆ 재시작 ì—†ì´ ì§„í–‰ +3. **로컬 íŒŒì¼ ì‚¬ìš©**: ë„¤íŠ¸ì›Œí¬ ê²½ë¡œë³´ë‹¤ 로컬 íŒŒì¼ ê²½ë¡œ 사용 권장 + +## 문제 í•´ê²° + +### Docker 관련 오류 +```bash +# Docker ë°ëª¬ 실행 í™•ì¸ +docker ps + +# Docker Desktop 시작 (Windows/Mac) +# Docker 서비스 시작 (Linux) +sudo systemctl start docker +``` + +### 권한 오류 +```bash +# Linux/Macì—서 스í¬ë¦½íЏ 실행 권한 +chmod +x tools/check-mermaid.sh + +# Docker 권한 (Linux) +sudo usermod -aG docker $USER +``` + +### 컨테ì´ë„ˆ ì´ë¯¸ì§€ 오류 +```bash +# ì´ë¯¸ì§€ 재다운로드 +docker pull minlag/mermaid-cli:latest + +# 기존 컨테ì´ë„ˆ ì‚­ì œ 후 재ìƒì„± +docker stop mermaid-cli && docker rm mermaid-cli +``` \ No newline at end of file diff --git a/claude/plantuml-guide.md b/claude/plantuml-guide.md new file mode 100644 index 0000000..5dd396c --- /dev/null +++ b/claude/plantuml-guide.md @@ -0,0 +1,82 @@ +# PlantUML문법검사가ì´ë“œ + +## 개요 + +PlantUML 다ì´ì–´ê·¸ëž¨ì˜ 문법 오류를 ì‚¬ì „ì— ê²€ì¶œí•˜ì—¬ ë Œë”ë§ ì‹¤íŒ¨ë¥¼ 방지하기 위한 ê°€ì´ë“œìž…니다. Docker 기반 PlantUML 서버를 활용하여 로컬ì—서 빠르게 ë¬¸ë²•ì„ ê²€ì¦í•  수 있습니다. + +## PlantUML 서버 설치 검사 + +### Docker로 PlantUML 서버 실행 + +```bash +# PlantUML 서버가 실행 중ì¸ì§€ í™•ì¸ +docker ps | grep plantuml + +# PlantUML 서버가 없으면 설치 ë° ì‹¤í–‰ +docker run -d --name plantuml -p 38080:8080 plantuml/plantuml-server:latest + +# 서버 ìƒíƒœ í™•ì¸ +docker logs plantuml +``` + +## 문법 검사 방법 +현재 OSì— ë§žê²Œ 수행. + +### Linux/macOS 버전 + +1. tools/check-plantuml.sh íŒŒì¼ ì¡´ìž¬ 여부 í™•ì¸ +2. 스í¬ë¦½íЏ 파ì¼ì´ 없으면 "PlantUML문법검사기(Linux/Mac)"를 tools/check-plantuml.sh 파ì¼ë¡œ 다운로드하여 스í¬ë¦½íЏ 파ì¼ì„ 만듦 +3. 스í¬ë¦½íЏ 파ì¼ì´ 있으면 ê·¸ 스í¬ë¦½íЏ 파ì¼ì„ ì´ìš©í•˜ì—¬ 검사 + +### Windows PowerShell 버전 +**스í¬ë¦½íЏ 파ì¼(tools/check-plantuml.ps1)ì„ ì´ìš©í•˜ì—¬ 수행**. + +1. tools/check-plantuml.ps1 íŒŒì¼ ì¡´ìž¬ 여부 í™•ì¸ +2. 스í¬ë¦½íЏ 파ì¼ì´ 없으면 "PlantUML문법검사기(Window)"를 tools/check-plantuml.ps1 파ì¼ë¡œ 다운로드하여 스í¬ë¦½íЏ 파ì¼ì„ 만듦 +3. 스í¬ë¦½íЏ 파ì¼ì´ 있으면 ê·¸ 스í¬ë¦½íЏ 파ì¼ì„ ì´ìš©í•˜ì—¬ 검사 + +### 검사 ê²°ê³¼ í•´ì„ + +| 출력 | ì˜ë¯¸ | ëŒ€ì‘ ë°©ë²• | +|------|------|-----------| +| 출력 ì—†ìŒ | 문법 오류 ì—†ìŒ âœ… | ì •ìƒ, ë Œë”ë§ ê°€ëŠ¥ | +| "Some diagram description contains errors" | 오류 존재 ⌠| 파ì´í”„ ë°©ì‹ìœ¼ë¡œ ìƒì„¸ í™•ì¸ | +| "ERROR" + ë¼ì¸ 번호 | 특정 ë¼ì¸ 오류 ⌠| 해당 ë¼ì¸ 수정 | +| "Error line X in file" | X번째 줄 오류 ⌠| 해당 ë¼ì¸ 문법 í™•ì¸ | + +## 화살표 문법 규칙 + +### 시퀀스 다ì´ì–´ê·¸ëž¨ 올바른 화살표 사용법 + +```plantuml +@startuml +' 올바른 사용법 ✅ +A -> B: ë™ê¸° 메시지 (실선) +A ->> B: 비ë™ê¸° 메시지 (실선, 열린 화살촉) +A -->> B: 비ë™ê¸° ì‘답 (ì ì„ , 열린 화살촉) +A --> B: ì ì„  화살표 (ì¼ë°˜) +A <-- B: ì‘답 (ì ì„ ) +A ->x B: 실패/ê±°ë¶€ (X 표시) +A ->>o B: 비ë™ê¸° 열린 ì› + +' ìž˜ëª»ëœ ì‚¬ìš©ë²• ⌠+A ..> B: ' 오류! sequence diagramì—서 유효하지 ì•ŠìŒ +@enduml +``` + +### í´ëž˜ìФ 다ì´ì–´ê·¸ëž¨ 화살표 + +```plantuml +@startuml +' í´ëž˜ìФ 다ì´ì–´ê·¸ëž¨ì—서는 ..> 사용 가능 +ClassA ..> ClassB : ì˜ì¡´ì„± (ì ì„ ) +ClassC --> ClassD : ì—°ê´€ (ì ì„ ) +@enduml +``` + +### 화살표 문법 주ì˜ì‚¬í•­ + +1. **`..>`는 sequence diagramì—서 사용 금지** +2. 비ë™ê¸° 메시지는 `->>` ë˜ëŠ” `-->>` 사용 +3. ë™ê¸°/비ë™ê¸°ë¥¼ 명확히 구분하여 ì¼ê´€ë˜ê²Œ 사용 +4. 다ì´ì–´ê·¸ëž¨ 타입별로 유효한 화살표가 다름 \ No newline at end of file diff --git a/claude/sequence-inner-design.md b/claude/sequence-inner-design.md new file mode 100644 index 0000000..586c62c --- /dev/null +++ b/claude/sequence-inner-design.md @@ -0,0 +1,76 @@ +# 내부시퀀스설계 ê°€ì´ë“œ + +[요청사항] +- <작성ì›ì¹™>ì„ ì¤€ìš©í•˜ì—¬ 설계 +- <작성순서>ì— ë”°ë¼ ì„¤ê³„ +- [결과파ì¼] ì•ˆë‚´ì— ë”°ë¼ íŒŒì¼ ìž‘ì„± + +[ê°€ì´ë“œ] +<작성ì›ì¹™> +- **유저스토리와 매칭**ë˜ì–´ì•¼ 함. **불필요한 추가 설계 금지** +- **외부시퀀스설계서ì—서 설계한 플로우와 ì¼ì¹˜**해야 함 +- UI/UXì„¤ê³„ì„œì˜ 'ì‚¬ìš©ìž í”Œë¡œìš°'참조하여 설계 +- 마ì´í¬ë¡œì„œë¹„스 ë‚´ë¶€ì˜ ì²˜ë¦¬ íë¦„ì„ í‘œì‹œ +- **ê° ì„œë¹„ìŠ¤-시나리오별로 분리하여 ê°ê° 작성** +- ê° ì„œë¹„ìŠ¤ë³„ 주요 시나리오마다 ë…립ì ì¸ 시퀀스 설계 수행 +- 프론트엔드와 백엔드 ì±…ìž„ 분리: 프론트엔드ì—서 í•  수 있는 ê²ƒì€ ë°±ì—”ë“œë¡œ 요청 안하게 함 +- 표현 요소 + - **API ë ˆì´ì–´**: 해당 ì‹œë‚˜ë¦¬ì˜¤ì˜ ëª¨ë“  관련 엔드í¬ì¸íЏ + - **비즈니스 ë ˆì´ì–´**: Controller → Service → Domain ë‚´ë¶€ 플로우 + - **ë°ì´í„° ë ˆì´ì–´**: Repository, Cache, External API ì ‘ê·¼ + - **ì¸í”„ë¼ ë ˆì´ì–´**: 메시지 í, ì´ë²¤íЏ, 로깅 등 +- 다ì´ì–´ê·¸ëž¨ 구성 + - **참여ìž(Actor)**: Controller, Service, Repository, Cache, External API + - **ìƒëª…ì„ (Lifeline)**: ê° ì°¸ì—¬ìžì˜ í™œë™ êµ¬ê°„ + - **메시지(Message)**: ë™ê¸°(→)/비ë™ê¸°(-->) 호출 구분 + - **활성화 박스**: 처리 ì¤‘ì¸ ì‹œê°„ 구간 표시 + - **노트**: 중요한 비즈니스 로ì§ì´ë‚˜ ê¸°ìˆ ì  ê³ ë ¤ì‚¬í•­ 설명 +- 참여ìžê°€ 서비스 내부가 아닌 다른 마ì´í¬ë¡œ 서비스, 외부시스템, ì¸í”„ë¼ ì»´í¬ë„ŒíŠ¸ë©´ ì°¸ì—¬ìž ì´ë¦„ ëì— '<>'를 붙임 + 예) database "Redis Cache<>" as cache + +<작성순서> +- 준비: + - 유저스토리, UI/UX설계서, 외부시퀀스설계서 ë¶„ì„ ë° ì´í•´ + - "@analyze --play" í”„ë¡œí† íƒ€ìž…ì´ ìžˆëŠ” 경우 웹브ë¼ìš°ì €ì—서 실행하여 서비스 ì´í•´ +- 실행: + - <시나리오 분류 ê°€ì´ë“œ>ì— ë”°ë¼ ê° ì„œë¹„ìŠ¤ë³„ë¡œ 시나리오 분류 + - 내부시퀀스설계서 작성 + - <병렬수행>ê°€ì´ë“œì— ë”°ë¼ ë™ì‹œ 수행 + - **PlantUML 스í¬ë¦½íЏ íŒŒì¼ ìƒì„± 즉시 검사 실행**: 'PlantUML 문법 검사 ê°€ì´ë“œ' 준용 +- 검토: + - <작성ì›ì¹™> 준수 검토 + - 스쿼드 íŒ€ì› ë¦¬ë·°: ëˆ„ë½ ë° ê°œì„  사항 검토 + - 수정 사항 ì„ íƒ ë° ë°˜ì˜ + +<시나리오 분류 ê°€ì´ë“œ> +- 시나리오 ì‹ë³„ 방법 + - **유저스토리 기반**: ê° ìœ ì €ìŠ¤í† ë¦¬ë¥¼ 기준으로 시나리오 ë„ì¶œ + - **비즈니스 기능 단위**: í•˜ë‚˜ì˜ ì™„ì „í•œ 비즈니스 ê¸°ëŠ¥ì„ ìˆ˜í–‰í•˜ëŠ” 단위로 분류 +- 시나리오별 설계 ì›ì¹™ + - **ë‹¨ì¼ ì±…ìž„**: í•˜ë‚˜ì˜ ì‹œë‚˜ë¦¬ì˜¤ëŠ” í•˜ë‚˜ì˜ ëª…í™•í•œ 비즈니스 목ì ì„ ê°€ì§ + - **완전성**: 해당 ì‹œë‚˜ë¦¬ì˜¤ì˜ ëª¨ë“  API와 ë‚´ë¶€ 처리를 í¬í•¨ + - **ë…립성**: ê° ì‹œë‚˜ë¦¬ì˜¤ëŠ” ë…립ì ìœ¼ë¡œ ì´í•´ 가능해야 함 + - **ì¼ê´€ì„±**: ë™ì¼í•œ 아키í…처 ë ˆì´ì–´ 표현 ë°©ì‹ ì‚¬ìš© +- 시나리오 명명 규칙 + - **케밥-ì¼€ì´ìФ 사용**: entity action 형태. 한글로 작성 (예: ì‚¬ìš©ìž ë“±ë¡, 주문 처리) + - **ë™ì‚¬í˜• ì•¡ì…˜**: 실제 수행하는 ìž‘ì—…ì„ ëª…í™•ížˆ 표현 + - **ì¼ê´€ëœ 용어**: 프로ì íЏ ë‚´ì—서 ë™ì¼í•œ 용어 사용 + +<병렬수행> +- **서브 ì—ì´ì „트를 활용한 병렬 작성 필수** +- 서비스별 ë…립ì ì¸ ì—ì´ì „트가 ê° ë‚´ë¶€ì‹œí€€ìŠ¤ì„¤ê³„ë¥¼ ë™ì‹œì— 작업 +- 모든 설계 완료 후 ì „ì²´ ê²€ì¦ + +[참고ìžë£Œ] +- 유저스토리 +- UI/UX설계서 +- 외부시퀀스설계서 +- 프로토타입 + +[예시] +- ë§í¬: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/samples/sample-시퀀스설계서(ë‚´ë¶€).puml + +[결과파ì¼] +- design/backend/sequence/inner/{서비스명}-{시나리오}.puml +- ì„œë¹„ìŠ¤ëª…ì€ ì˜ì–´ë¡œ ì‹œë‚˜ë¦¬ì˜¤ëª…ì€ í•œê¸€ë¡œ 작성 + diff --git a/claude/sequence-outer-design.md b/claude/sequence-outer-design.md new file mode 100644 index 0000000..fc60efe --- /dev/null +++ b/claude/sequence-outer-design.md @@ -0,0 +1,54 @@ +# 외부시퀀스설계가ì´ë“œ + +[요청사항] +- <작성ì›ì¹™>ì„ ì¤€ìš©í•˜ì—¬ 설계 +- <작성순서>ì— ë”°ë¼ ì„¤ê³„ +- [결과파ì¼] ì•ˆë‚´ì— ë”°ë¼ íŒŒì¼ ìž‘ì„± + +[ê°€ì´ë“œ] +<작성ì›ì¹™> +- **유저스토리와 매칭**ë˜ì–´ì•¼ 함. **불필요한 추가 설계 금지** +- **논리아키í…ì²˜ì— ì •ì˜í•œ 참여ìžì™€ ì¼ì¹˜**해야 함 +- UI/UXì„¤ê³„ì„œì˜ 'ì‚¬ìš©ìž í”Œë¡œìš°'참조하여 설계 +- **해당 í”Œë¡œìš°ì— ì°¸ì—¬í•˜ëŠ”** 프론트엔드, 모든 서비스, ì¸í”„ë¼ ì»´í¬ë„ŒíЏ(예: Gateway, Message Queue, Database), ì™¸ë¶€ì‹œìŠ¤í…œì„ ì°¸ì—¬ìžë¡œ 추가 +- í”Œë¡œìš°ì— ì°¸ì—¬ìžë“¤ì˜ ì—­í• ê³¼ ì±…ìž„ì„ ëª…í™•ížˆ 표시 +- 플로우 ìˆ˜í–‰ì— í•„ìš”í•œ 프론트엔드부터 End-to-End í˜¸ì¶œì„ ìˆœì„œëŒ€ë¡œ 표시하고 한글 설명 추가 +- 마ì´í¬ë¡œì„œë¹„스 ë‚´ë¶€ì˜ ì²˜ë¦¬ íë¦„ì€ í‘œì‹œí•˜ì§€ ì•ŠìŒ +- ë™ê¸°/비ë™ê¸° 통신 구분 (실선/ì ì„ ) +- ìºì‹œ, í 등 ì¸í”„ë¼ ì»´í¬ë„ŒíŠ¸ì™€ì˜ ìƒí˜¸ìž‘ìš© í¬í•¨ +<작성순서> +- 준비: + - 유저스토리, UI/UX설계서, 논리아키í…처 ë¶„ì„ ë° ì´í•´ + - "@analyze --play" í”„ë¡œí† íƒ€ìž…ì´ ìžˆëŠ” 경우 웹브ë¼ìš°ì €ì—서 실행하여 서비스 ì´í•´ +- 실행: + - <플로우 분류기준>ì— ë”°ë¼ ìµœì ì˜ 플로우를 결정함 + - 외부시퀀스설계서 작성 + - <병렬수행>ê°€ì´ë“œì— ë”°ë¼ ë³‘ë ¬ 수행 + - **PlantUML 스í¬ë¦½íЏ íŒŒì¼ ìƒì„± 즉시 검사 실행**: 'PlantUML 문법 검사 ê°€ì´ë“œ' 준용 +- 검토: + - <작성ì›ì¹™> 준수 검토 + - 스쿼드 íŒ€ì› ë¦¬ë·°: ëˆ„ë½ ë° ê°œì„  사항 검토 + - 수정 사항 ì„ íƒ ë° ë°˜ì˜ +<플로우 분류기준> +- **핵심 비즈니스 플로우별**: 사용ìžê°€ 목표를 달성하기 위한 주요 업무 í름 +- **통합 패턴별**: ë™ê¸°/비ë™ê¸°, ìºì‹œ 활용, 외부 ì—°ë™ ë“± ê¸°ìˆ ì  í†µí•© ë°©ì‹ +- **ì‚¬ìš©ìž ì‹œë‚˜ë¦¬ì˜¤ë³„**: 엔드투엔드 ì‚¬ìš©ìž ê²½í—˜ 기준 +- **ë°ì´í„° í름별**: ë°ì´í„°ì˜ ìƒì„±, 변환, 저장 과정 +<병렬수행> +- **서브 ì—ì´ì „트를 활용한 병렬 작성 필수** +- 서비스별 ë…립ì ì¸ ì—ì´ì „트가 ê° ì™¸ë¶€ì‹œí€€ìŠ¤ì„¤ê³„ë¥¼ ë™ì‹œì— 작업 +- 모든 설계 완료 후 ì „ì²´ ê²€ì¦ + +[참고ìžë£Œ] +- 유저스토리 +- UI/UX설계서 +- 논리아키í…처 +- 프로토타입 + +[예시] +- ë§í¬: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/samples/sample-시퀀스설계서(외부).puml + +[결과파ì¼] +- **주요 비즈니스 플로우별로 파ì¼ì„ 분리하여 작성** +- í”Œë¡œìš°ëª…ì€ í•œê¸€ë¡œ 네ì´ë° +- 위치: design/backend/sequence/outer/{플로우명}.puml diff --git a/claude/standard_comment.md b/claude/standard_comment.md new file mode 100644 index 0000000..5200015 --- /dev/null +++ b/claude/standard_comment.md @@ -0,0 +1,518 @@ +# 개발주ì„표준 ê°€ì´ë“œ + +## 📋 개요 + +ì´ ë¬¸ì„œëŠ” CMS 프로ì íŠ¸ì˜ JavaDoc ì£¼ì„ ìž‘ì„± í‘œì¤€ì„ ì •ì˜í•©ë‹ˆë‹¤. ì¼ê´€ëœ ì£¼ì„ ìŠ¤íƒ€ì¼ì„ 통해 ì½”ë“œì˜ ê°€ë…성과 ìœ ì§€ë³´ìˆ˜ì„±ì„ í–¥ìƒì‹œí‚¤ëŠ” ê²ƒì„ ëª©í‘œë¡œ 합니다. + +## 🎯 ì£¼ì„ ìž‘ì„± ì›ì¹™ + +### 1. **기본 ì›ì¹™** +- **명확성**: ì½”ë“œì˜ ì˜ë„와 ë™ìž‘ì„ ëª…í™•í•˜ê²Œ 설명 +- **ì¼ê´€ì„±**: 프로ì íЏ ì „ì²´ì—서 ë™ì¼í•œ ìŠ¤íƒ€ì¼ ì ìš© +- **완전성**: 모든 public 메서드와 í´ëž˜ìŠ¤ì— ì£¼ì„ ìž‘ì„± +- **최신성**: 코드 변경 시 주ì„ë„ í•¨ê»˜ ì—…ë°ì´íЏ + +### 2. **ì£¼ì„ ëŒ€ìƒ** +- **필수**: public í´ëž˜ìФ, ì¸í„°íŽ˜ì´ìФ, 메서드 +- **권장**: protected 메서드, 중요한 필드 +- **ì„ íƒ**: private 메서드 (복잡한 로ì§ì¸ 경우) + +## 📠JavaDoc 기본 문법 + +### 1. **기본 구조** +```java +/** + * í´ëž˜ìŠ¤ë‚˜ ë©”ì„œë“œì˜ ê°„ë‹¨í•œ 설명 (첫 번째 문장) + * + *

      ìƒì„¸í•œ ì„¤ëª…ì´ í•„ìš”í•œ 경우 ì—¬ê¸°ì— ìž‘ì„±í•©ë‹ˆë‹¤.

      + * + * @param paramName 파ë¼ë¯¸í„° 설명 + * @return 반환값 설명 + * @throws ExceptionType 예외 ìƒí™© 설명 + * @since 1.0 + * @author 작성ìžëª… + * @see 관련í´ëž˜ìФ#메서드 + */ +``` + +### 2. **주요 JavaDoc 태그** + +| 태그 | 설명 | 사용 위치 | +|------|------|-----------| +| `@param` | 메서드 파ë¼ë¯¸í„° 설명 | 메서드 | +| `@return` | 반환값 설명 | 메서드 | +| `@throws` | 예외 ìƒí™© 설명 | 메서드 | +| `@since` | ë„ìž… 버전 | í´ëž˜ìФ, 메서드 | +| `@author` | ìž‘ì„±ìž | í´ëž˜ìФ | +| `@version` | 버전 ì •ë³´ | í´ëž˜ìФ | +| `@see` | 관련 항목 참조 | 모든 ê³³ | +| `@apiNote` | API 사용 시 주ì˜ì‚¬í•­ | 메서드 | +| `@implNote` | 구현 관련 참고사항 | 메서드 | + +## 🎨 HTML 태그 활용 ê°€ì´ë“œ + +### 1. **HTML 태그 사용 ì´ìœ ** + +JavaDocì€ ì†ŒìŠ¤ì½”ë“œ 주ì„ì„ íŒŒì‹±í•˜ì—¬ **HTML í˜•íƒœì˜ API 문서**를 ìžë™ ìƒì„±í•©ë‹ˆë‹¤. HTML 태그를 사용하면: + +- **ê°€ë…성 í–¥ìƒ**: êµ¬ì¡°í™”ëœ ë¬¸ì„œë¡œ ì´í•´í•˜ê¸° 쉬움 +- **ìžë™ 문서화**: JavaDoc ë„구가 ì˜ˆìœ HTML 문서 ìƒì„± +- **IDE ì§€ì›**: 개발 ë„구ì—서 리치 í…스트로 표시 +- **표준 준수**: Oracle JavaDoc ìŠ¤íƒ€ì¼ ê°€ì´ë“œ 준수 + +### 2. **ìžì£¼ 사용ë˜ëŠ” HTML 태그** + +#### **í…스트 서ì‹** +```java +/** + *

      단ë½ì„ 구분할 때 사용합니다.

      + * 중요한 ë‚´ìš©ì„ ê°•ì¡°í•  때 사용합니다. + * ì´íƒ¤ë¦­ì²´ë¡œ 표시할 때 사용합니다. + * method()와 ê°™ì€ ì½”ë“œë¥¼ 표시할 때 사용합니다. + */ +``` + +#### **ëª©ë¡ ìž‘ì„±** +```java +/** + *

      주요 기능:

      + *
        + *
      • 첫 번째 기능
      • + *
      • ë‘ ë²ˆì§¸ 기능
      • + *
      • 세 번째 기능
      • + *
      + * + *

      처리 과정:

      + *
        + *
      1. 첫 번째 단계
      2. + *
      3. ë‘ ë²ˆì§¸ 단계
      4. + *
      5. 세 번째 단계
      6. + *
      + */ +``` + +#### **코드 블ë¡** +```java +/** + *

      사용 예시:

      + *
      + * AuthController controller = new AuthController();
      + * LoginRequest request = new LoginRequest("user", "password");
      + * ResponseEntity<LoginResponse> response = controller.login(request);
      + * 
      + */ +``` + +#### **í…Œì´ë¸”** +```java +/** + *

      HTTP ìƒíƒœ 코드:

      + * + * + * + * + * + *
      ìƒíƒœ 코드설명
      200성공
      400ìž˜ëª»ëœ ìš”ì²­
      401ì¸ì¦ 실패
      + */ +``` + +### 3. **HTML 태그 사용 규칙** + +- **<와 >**: 제네릭 타입 표현 시 `<T>` 사용 +- **줄바꿈**: `
      ` 태그 사용 (ê°€ê¸‰ì  `

      ` 태그 권장) +- **ë§í¬**: `{@link ClassName#methodName}` 사용 +- **ì¸ë¼ì¸ 코드**: `{@code variableName}` ë˜ëŠ” `` 사용 + +## 📋 í´ëž˜ìФ ì£¼ì„ í‘œì¤€ + +### 1. **í´ëž˜ìФ ì£¼ì„ í…œí”Œë¦¿** +```java +/** + * í´ëž˜ìŠ¤ì˜ ê°„ë‹¨í•œ 설명 + * + *

      í´ëž˜ìŠ¤ì˜ ìƒì„¸í•œ 설명과 목ì ì„ ì—¬ê¸°ì— ìž‘ì„±í•©ë‹ˆë‹¤.

      + * + *

      주요 기능:

      + *
        + *
      • 기능 1
      • + *
      • 기능 2
      • + *
      • 기능 3
      • + *
      + * + *

      사용 예시:

      + *
      + * ClassName instance = new ClassName();
      + * instance.someMethod();
      + * 
      + * + *

      주ì˜ì‚¬í•­:

      + *
        + *
      • 주ì˜ì‚¬í•­ 1
      • + *
      • 주ì˜ì‚¬í•­ 2
      • + *
      + * + * @author 작성ìžëª… + * @version 1.0 + * @since 2024-01-01 + * + * @see 관련í´ëž˜ìФ1 + * @see 관련í´ëž˜ìФ2 + */ +public class ClassName { + // ... +} +``` + +### 2. **Controller í´ëž˜ìФ ì£¼ì„ ì˜ˆì‹œ** +```java +/** + * ì‚¬ìš©ìž ê´€ë¦¬ API 컨트롤러 + * + *

      ì‚¬ìš©ìž ë“±ë¡, 조회, 수정, ì‚­ì œ ê¸°ëŠ¥ì„ ì œê³µí•˜ëŠ” REST API 컨트롤러입니다.

      + * + *

      주요 기능:

      + *
        + *
      • ì‚¬ìš©ìž ë“±ë¡ ë° ì¸ì¦
      • + *
      • ì‚¬ìš©ìž ì •ë³´ 조회 ë° ìˆ˜ì •
      • + *
      • ì‚¬ìš©ìž ê¶Œí•œ 관리
      • + *
      + * + *

      API 엔드í¬ì¸íЏ:

      + *
        + *
      • POST /api/users - ì‚¬ìš©ìž ë“±ë¡
      • + *
      • GET /api/users/{id} - ì‚¬ìš©ìž ì¡°íšŒ
      • + *
      • PUT /api/users/{id} - ì‚¬ìš©ìž ìˆ˜ì •
      • + *
      • DELETE /api/users/{id} - ì‚¬ìš©ìž ì‚­ì œ
      • + *
      + * + *

      보안 고려사항:

      + *
        + *
      • 모든 엔드í¬ì¸íŠ¸ëŠ” ì¸ì¦ì´ 필요합니다
      • + *
      • ê°œì¸ì •ë³´ 처리 시 ë°ì´í„° 마스킹 ì ìš©
      • + *
      • 입력값 ê²€ì¦ ë° XSS ë°©ì§€
      • + *
      + * + * @author cms-team + * @version 1.0 + * @since 2024-01-01 + * + * @see UserService + * @see UserRepository + * @see UserDTO + */ +@RestController +@RequestMapping("/api/users") +public class UserController { + // ... +} +``` + +## 📋 메서드 ì£¼ì„ í‘œì¤€ + +### 1. **메서드 ì£¼ì„ í…œí”Œë¦¿** +```java +/** + * ë©”ì„œë“œì˜ ê°„ë‹¨í•œ 설명 + * + *

      ë©”ì„œë“œì˜ ìƒì„¸í•œ 설명과 ë™ìž‘ì„ ì—¬ê¸°ì— ìž‘ì„±í•©ë‹ˆë‹¤.

      + * + *

      처리 과정:

      + *
        + *
      1. 첫 번째 단계
      2. + *
      3. ë‘ ë²ˆì§¸ 단계
      4. + *
      5. 세 번째 단계
      6. + *
      + * + *

      주ì˜ì‚¬í•­:

      + *
        + *
      • 주ì˜ì‚¬í•­ 1
      • + *
      • 주ì˜ì‚¬í•­ 2
      • + *
      + * + * @param param1 첫 번째 파ë¼ë¯¸í„° 설명 + * - 추가 ì„¤ëª…ì´ í•„ìš”í•œ 경우 + * @param param2 ë‘ ë²ˆì§¸ 파ë¼ë¯¸í„° 설명 + * + * @return 반환값 설명 + * - 성공 시: 설명 + * - 실패 시: 설명 + * + * @throws ExceptionType1 예외 ìƒí™© 1 설명 + * @throws ExceptionType2 예외 ìƒí™© 2 설명 + * + * @apiNote API 사용 시 주ì˜ì‚¬í•­ + * + * @see 관련메서드1 + * @see 관련메서드2 + * + * @since 1.0 + */ +public ReturnType methodName(Type param1, Type param2) { + // ... +} +``` + +### 2. **API 메서드 ì£¼ì„ ì˜ˆì‹œ** +```java +/** + * ì‚¬ìš©ìž ë¡œê·¸ì¸ ì²˜ë¦¬ + * + *

      ì‚¬ìš©ìž ID와 비밀번호를 ê²€ì¦í•˜ì—¬ JWT 토í°ì„ ìƒì„±í•©ë‹ˆë‹¤.

      + * + *

      처리 과정:

      + *
        + *
      1. 입력값 ê²€ì¦ (@Valid 어노테ì´ì…˜)
      2. + *
      3. ì‚¬ìš©ìž ì¸ì¦ ì •ë³´ 확ì¸
      4. + *
      5. JWT í† í° ìƒì„±
      6. + *
      7. ì‚¬ìš©ìž ì„¸ì…˜ 시작
      8. + *
      9. ë¡œê·¸ì¸ ë©”íŠ¸ë¦­ ì—…ë°ì´íЏ
      10. + *
      + * + *

      보안 고려사항:

      + *
        + *
      • 비밀번호는 BCrypt로 ì•”í˜¸í™”ëœ ê°’ê³¼ 비êµ
      • + *
      • ë¡œê·¸ì¸ ì‹¤íŒ¨ 시 ìƒì„¸ ì •ë³´ 노출 ë°©ì§€
      • + *
      • ë¡œê·¸ì¸ ì‹œë„ ë¡œê·¸ 기ë¡
      • + *
      + * + * @param request ë¡œê·¸ì¸ ìš”ì²­ ì •ë³´ + * - username: ì‚¬ìš©ìž ID (3-50ìž, 필수) + * - password: 비밀번호 (6-100ìž, 필수) + * + * @return ResponseEntity<LoginResponse> ë¡œê·¸ì¸ ì‘답 ì •ë³´ + * - 성공 시: 200 OK + JWT 토í°, ì‚¬ìš©ìž ì—­í• , 만료 시간 + * - 실패 시: 401 Unauthorized + ì—러 메시지 + * + * @throws InvalidCredentialsException ì¸ì¦ ì •ë³´ê°€ 올바르지 ì•Šì€ ê²½ìš° + * @throws RuntimeException ë¡œê·¸ì¸ ì²˜ë¦¬ 중 시스템 오류 ë°œìƒ ì‹œ + * + * @apiNote ë³´ì•ˆìƒ ì´ìœ ë¡œ ë¡œê·¸ì¸ ì‹¤íŒ¨ 시 구체ì ì¸ 실패 사유를 반환하지 않습니다. + * + * @see AuthService#login(LoginRequest) + * @see UserSessionService#startSession(String, String, java.time.Instant) + * + * @since 1.0 + */ +@PostMapping("/login") +public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + // ... +} +``` + +## 📋 필드 ì£¼ì„ í‘œì¤€ + +### 1. **필드 ì£¼ì„ í…œí”Œë¦¿** +```java +/** + * í•„ë“œì˜ ê°„ë‹¨í•œ 설명 + * + *

      í•„ë“œì˜ ìƒì„¸í•œ 설명과 ìš©ë„를 ì—¬ê¸°ì— ìž‘ì„±í•©ë‹ˆë‹¤.

      + * + *

      주ì˜ì‚¬í•­:

      + *
        + *
      • 주ì˜ì‚¬í•­ 1
      • + *
      • 주ì˜ì‚¬í•­ 2
      • + *
      + * + * @since 1.0 + */ +private final ServiceType serviceName; +``` + +### 2. **ì˜ì¡´ì„± 주입 필드 예시** +```java +/** + * ì¸ì¦ 서비스 + * + *

      ì‚¬ìš©ìž ë¡œê·¸ì¸/로그아웃 처리 ë° JWT í† í° ê´€ë¦¬ë¥¼ 담당합니다.

      + * + *

      주요 기능:

      + *
        + *
      • ì‚¬ìš©ìž ì¸ì¦ ì •ë³´ ê²€ì¦
      • + *
      • JWT í† í° ìƒì„± ë° ê²€ì¦
      • + *
      • 로그ì¸/로그아웃 처리
      • + *
      + * + * @see AuthService + * @since 1.0 + */ +private final AuthService authService; +``` + +## 📋 예외 í´ëž˜ìФ ì£¼ì„ í‘œì¤€ + +```java +/** + * ì‚¬ìš©ìž ì¸ì¦ 실패 예외 + * + *

      ë¡œê·¸ì¸ ì‹œ ì‚¬ìš©ìž ID ë˜ëŠ” 비밀번호가 올바르지 ì•Šì„ ë•Œ ë°œìƒí•˜ëŠ” 예외입니다.

      + * + *

      ë°œìƒ ìƒí™©:

      + *
        + *
      • 존재하지 않는 ì‚¬ìš©ìž ID
      • + *
      • ìž˜ëª»ëœ ë¹„ë°€ë²ˆí˜¸
      • + *
      • 계정 잠금 ìƒíƒœ
      • + *
      + * + *

      처리 방법:

      + *
        + *
      • 사용ìžì—게 ì¼ë°˜ì ì¸ 오류 메시지 표시
      • + *
      • 보안 ë¡œê·¸ì— ìƒì„¸ ì •ë³´ 기ë¡
      • + *
      • 브루트 í¬ìФ 공격 ë°©ì§€ ë¡œì§ ì‹¤í–‰
      • + *
      + * + * @author cms-team + * @version 1.0 + * @since 2024-01-01 + * + * @see AuthService + * @see SecurityException + */ +public class InvalidCredentialsException extends RuntimeException { + // ... +} +``` + +## 📋 ì¸í„°íŽ˜ì´ìФ ì£¼ì„ í‘œì¤€ + +```java +/** + * ì‚¬ìš©ìž ì¸ì¦ 서비스 ì¸í„°íŽ˜ì´ìФ + * + *

      ì‚¬ìš©ìž ë¡œê·¸ì¸, 로그아웃, í† í° ê´€ë¦¬ 등 ì¸ì¦ 관련 ê¸°ëŠ¥ì„ ì •ì˜í•©ë‹ˆë‹¤.

      + * + *

      구현 í´ëž˜ìФ:

      + *
        + *
      • {@link AuthServiceImpl} - 기본 구현체
      • + *
      • {@link LdapAuthService} - LDAP ì—°ë™ êµ¬í˜„ì²´
      • + *
      + * + *

      주요 기능:

      + *
        + *
      • ì‚¬ìš©ìž ì¸ì¦ ë° í† í° ìƒì„±
      • + *
      • 로그아웃 ë° í† í° ë¬´íš¨í™”
      • + *
      • í† í° ìœ íš¨ì„± ê²€ì¦
      • + *
      + * + * @author cms-team + * @version 1.0 + * @since 2024-01-01 + * + * @see AuthServiceImpl + * @see TokenProvider + */ +public interface AuthService { + // ... +} +``` + +## 📋 Enum ì£¼ì„ í‘œì¤€ + +```java +/** + * ì‚¬ìš©ìž ì—­í•  열거형 + * + *

      시스템 사용ìžì˜ 권한 ìˆ˜ì¤€ì„ ì •ì˜í•©ë‹ˆë‹¤.

      + * + *

      권한 계층:

      + *
        + *
      1. {@link #ADMIN} - 최고 ê´€ë¦¬ìž ê¶Œí•œ
      2. + *
      3. {@link #MANAGER} - ê´€ë¦¬ìž ê¶Œí•œ
      4. + *
      5. {@link #USER} - ì¼ë°˜ ì‚¬ìš©ìž ê¶Œí•œ
      6. + *
      + * + * @author cms-team + * @version 1.0 + * @since 2024-01-01 + */ +public enum Role { + + /** + * 시스템 ê´€ë¦¬ìž + * + *

      모든 시스템 ê¸°ëŠ¥ì— ëŒ€í•œ ì ‘ê·¼ ê¶Œí•œì„ ê°€ì§‘ë‹ˆë‹¤.

      + * + *

      주요 권한:

      + *
        + *
      • ì‚¬ìš©ìž ê´€ë¦¬
      • + *
      • 시스템 설정
      • + *
      • 모든 ë°ì´í„° ì ‘ê·¼
      • + *
      + */ + ADMIN, + + /** + * ê´€ë¦¬ìž + * + *

      ì œí•œëœ ê´€ë¦¬ ê¸°ëŠ¥ì— ëŒ€í•œ ì ‘ê·¼ ê¶Œí•œì„ ê°€ì§‘ë‹ˆë‹¤.

      + */ + MANAGER, + + /** + * ì¼ë°˜ ì‚¬ìš©ìž + * + *

      기본ì ì¸ 시스템 ê¸°ëŠ¥ì— ëŒ€í•œ ì ‘ê·¼ ê¶Œí•œì„ ê°€ì§‘ë‹ˆë‹¤.

      + */ + USER +} +``` + +## 📋 ì£¼ì„ ìž‘ì„± ì²´í¬ë¦¬ìŠ¤íŠ¸ + +### ✅ **í´ëž˜ìФ ì£¼ì„ ì²´í¬ë¦¬ìŠ¤íŠ¸** +- [ ] í´ëž˜ìŠ¤ì˜ ëª©ì ê³¼ ì—­í•  명시 +- [ ] 주요 기능 ëª©ë¡ ìž‘ì„± +- [ ] 사용 예시 코드 í¬í•¨ +- [ ] 주ì˜ì‚¬í•­ ë° ì œì•½ì‚¬í•­ 명시 +- [ ] @author, @version, @since 태그 작성 +- [ ] 관련 í´ëž˜ìФ @see 태그 추가 + +### ✅ **메서드 ì£¼ì„ ì²´í¬ë¦¬ìŠ¤íŠ¸** +- [ ] ë©”ì„œë“œì˜ ëª©ì ê³¼ ë™ìž‘ 설명 +- [ ] 처리 과정 단계별 설명 +- [ ] 모든 @param 태그 작성 +- [ ] @return 태그 작성 (void 메서드 제외) +- [ ] 가능한 예외 @throws 태그 작성 +- [ ] 보안 관련 주ì˜ì‚¬í•­ 명시 +- [ ] 관련 메서드 @see 태그 추가 + +### ✅ **HTML 태그 ì²´í¬ë¦¬ìŠ¤íŠ¸** +- [ ] 목ë¡ì€ `
        `, `
          `, `
        1. ` 태그 사용 +- [ ] 강조는 `` 태그 사용 +- [ ] ë‹¨ë½ êµ¬ë¶„ì€ `

          ` 태그 사용 +- [ ] 코드는 `` ë˜ëŠ” `

          ` 태그 사용
          +- [ ] 제네릭 íƒ€ìž…ì€ `<`, `>` 사용
          +
          +## 📋 ë„구 ë° ì„¤ì •
          +
          +### 1. **JavaDoc ìƒì„±**
          +```bash
          +# Gradle 프로ì íЏ
          +./gradlew javadoc
          +
          +# Maven 프로ì íЏ
          +mvn javadoc:javadoc
          +
          +# ì§ì ‘ 실행
          +javadoc -d docs -cp classpath src/**/*.java
          +```
          +
          +### 2. **IDE 설정**
          +- **IntelliJ IDEA**: Settings > Editor > Code Style > Java > JavaDoc
          +- **Eclipse**: Window > Preferences > Java > Code Style > Code Templates
          +- **VS Code**: Java Extension Pack + JavaDoc 플러그ì¸
          +
          +### 3. **ì •ì  ë¶„ì„ ë„구**
          +- **Checkstyle**: JavaDoc ëˆ„ë½ ê²€ì‚¬
          +- **SpotBugs**: ì£¼ì„ í’ˆì§ˆ 검사
          +- **SonarQube**: 문서화 품질 메트릭
          +
          +## 📋 참고 ìžë£Œ
          +
          +- [Oracle JavaDoc ê°€ì´ë“œ](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html)
          +- [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
          +- [Spring Framework ì£¼ì„ ìŠ¤íƒ€ì¼](https://github.com/spring-projects/spring-framework/wiki/Code-Style)
          +
          +---
          +
          +> **💡 íŒ**: ì´ ê°€ì´ë“œë¥¼ 팀 ë‚´ì—서 공유하고, 코드 리뷰 시 ì£¼ì„ í’ˆì§ˆë„ í•¨ê»˜ 검토하세요!
          \ No newline at end of file
          diff --git a/claude/standard_package_structure.md b/claude/standard_package_structure.md
          new file mode 100644
          index 0000000..81a4890
          --- /dev/null
          +++ b/claude/standard_package_structure.md
          @@ -0,0 +1,173 @@
          +패키지 구조 표준
          +
          +ë ˆì´ì–´ë“œ 아키í…처 패키지 구조
          +
          +├── {SERVICE}
          +│   ├── domain
          +│   ├── service
          +│   ├── controller
          +│   ├── dto
          +│   ├── repository
          +│   │   ├── jpa
          +│   │   └── entity
          +│   ├── config
          +└── common
          +        ├── dto
          +        ├── util
          +        ├── response
          +        └── exception
          +
          +Package명: 
          +- com.{ORG}.{ROOT}.{SERVICE}
          +예) com.unicorn.lifesub.mysub, com.unicorn.lifesub.common
          +
          +변수: 
          +- ORG: 회사 ë˜ëŠ” ì¡°ì§ëª…
          +- ROOT: Root Project 명
          +- SERVICE: 서비스명으로 Root Projectì˜ ì„œë¸Œ 프로ì íŠ¸ìž„
          +
          +
          +예시
          +
          +com.unicorn.lifesub.member
          + ├── MemberApplication.java
          + ├── controller
          + │   └── MemberController.java
          + ├── dto
          + │   ├── LoginRequest.java
          + │   ├── LogoutRequest.java
          + │   └── LogoutResponse.java  
          + ├── service
          + │   ├── MemberService.java
          + │   └── MemberServiceImpl.java
          + ├── domain
          + │   └── Member.java
          + ├── repository  
          + │   ├── entity
          + │   │   └── MemberEntity.java
          + │   └── jpa
          + │       └── MemberRepository.java
          + └── config
          +     ├── SecurityConfig.java
          +     ├── DataLoader.java
          +     ├── SwaggerConfig.java
          +     └── jwt
          +         ├── JwtAuthenticationFilter.java
          +         ├── JwtTokenProvider.java
          +         └── CustomUserDetailsService.java
          +
          +
          +í´ë¦° 아키í…처 패키지 구조 
          +
          +├── biz
          +│   ├── usecase
          +│   │   ├── in
          +│   │   ├── out
          +│   ├── service
          +│   └── domain
          +│   └── dto
          +├── infra
          +│   ├── controller
          +│   ├── dto
          +│   ├── gateway
          +│   │   ├── repository
          +│   │   └── entity
          +│   └── config    
          +
          +
          +Package명: 
          +- com.{ORG}.{ROOT}.{SERVICE}.biz
          +- com.{ORG}.{ROOT}.{SERVICE}.infra
          +예) com.unicorn.lifesub.mysub.biz, com.unicorn.lifesub.common
          +
          +변수: 
          +- ORG: 회사 ë˜ëŠ” ì¡°ì§ëª…
          +- ROOT: Root Project 명
          +- SERVICE: 서비스명으로 Root Projectì˜ ì„œë¸Œ 프로ì íŠ¸ìž„
          +
          +예시
          +
          +
          +com.unicorn.lifesub.mysub
          + ├── biz
          + │   ├── dto
          + │   │   ├── CategoryResponse.java
          + │   │   ├── ServiceListResponse.java
          + │   │   ├── MySubResponse.java
          + │   │   ├── SubDetailResponse.java
          + │   │   └── TotalFeeResponse.java
          + │   ├── service
          + │   │   ├── FeeLevel.java
          + │   │   └── MySubscriptionService.java
          + │   ├── usecase
          + │   │   ├── in
          + │   │   │   ├── CancelSubscriptionUseCase.java
          + │   │   │   ├── CategoryUseCase.java
          + │   │   │   ├── MySubscriptionsUseCase.java
          + │   │   │   ├── SubscribeUseCase.java
          + │   │   │   ├── SubscriptionDetailUseCase.java
          + │   │   │   └── TotalFeeUseCase.java
          + │   │   └── out 
          + │   │       ├── MySubscriptionReader.java
          + │   │       ├── MySubscriptionWriter.java
          + │   │       └── SubscriptionReader.java
          + │   └── domain
          + │       ├── Category.java
          + │       ├── MySubscription.java
          + │       └── Subscription.java
          + └── infra  
          +     ├── MySubApplication.java 
          +     ├── controller
          +     │   ├── CategoryController.java
          +     │   ├── MySubController.java
          +     │   └── ServiceController.java
          +     ├── config
          +     │   ├── DataLoader.java
          +     │   ├── SecurityConfig.java
          +     │   ├── SwaggerConfig.java
          +     │   └── jwt
          +     │       ├── JwtAuthenticationFilter.java
          +     │       └── JwtTokenProvider.java
          +     └── gateway
          +         ├── entity
          +         │   ├── CategoryEntity.java   
          +         │   ├── MySubscriptionEntity.java
          +         │   └── SubscriptionEntity.java
          +         ├── repository
          +         │   ├── CategoryJpaRepository.java
          +         │   ├── MySubscriptionJpaRepository.java
          +         │   └── SubscriptionJpaRepository.java  
          +         ├── MySubscriptionGateway.java
          +         └── SubscriptionGateway.java
          +
          +
          +---
          +
          +common 모듈 패키지 구조
          +
          +├── common
          +    ├── dto
          +    ├── entity
          +    ├── config
          +    ├── util
          +    └── exception
          +
          +
          +com.unicorn.lifesub.common
          + ├── dto
          + │   ├── ApiResponse.java
          + │   ├── JwtTokenDTO.java
          + │   ├── JwtTokenRefreshDTO.java
          + │   └── JwtTokenVerifyDTO.java
          + ├── config
          + │   └── JpaConfig.java
          + ├── entity
          + │   └── BaseTimeEntity.java        
          + ├── aop  
          + │   └── LoggingAspect.java
          + └── exception
          +     ├── ErrorCode.java
          +     ├── InfraException.java
          +     └── BusinessException.java
          +
          +
          diff --git a/claude/standard_testcode.md b/claude/standard_testcode.md
          new file mode 100644
          index 0000000..1eec6d5
          --- /dev/null
          +++ b/claude/standard_testcode.md
          @@ -0,0 +1,214 @@
          +1.TDD 기본 ì´í•´
          +
          +1) TDD ëª©ì   
          +   코드 품질 í–¥ìƒìœ¼ë¡œ 유지보수 비용 ì ˆê°
          +- 설계 품질 í–¥ìƒ: 테스트를 먼저 작성하면서 코드 구조와 ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 먼저 고민
          +- 회귀 버그 ë°©ì§€: 테스트 ìžë™í™”로 코드 변경 시 기존 ê¸°ëŠ¥ì˜ ì˜¤ìž‘ë™ì„ 빠르게 ê°ì§€
          +- ë¦¬íŒ©í† ë§ ê²€ì¦: 코드 개선 후 테스트 코드로 ê²€ì¦í•  수 있어 리팩토ë§ì— 대한 ìžì‹ ê° 확보
          +- 살아있는 문서: 테스트 ì½”ë“œì— ìƒ˜í”Œ ë°ì´í„°ë¥¼ ì´ìš©í•œ 예시가 있으므로 실제 ì½”ë“œì˜ ë™ìž‘ ë°©ì‹ì„ 문서화
          +
          +---  
          +
          +2) 테스트 유형
          +- 단위 테스트(Unit Test): 외부 기술요소(DB, 웹서버 등)ì™€ì˜ ì¸í„°íŽ˜ì´ìФ ì—†ì´ ë‹¨ìœ„ í´ëž˜ìŠ¤ì˜ í¼ë¸”릭 메소드 테스트
          +- 통합 테스트(Integration Test): ì¼ë¶€ 아키í…처 ì˜ì—­ì—서 외부 기술 요소와 ì¸í„°íŽ˜ì´ìŠ¤ê¹Œì§€ 테스트
          +- E2E 테스트(E2E Test): 모든 아키í…처 ì˜ì—­ì—서 외부 기술 요소와 ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 테스트
          +
          +* 아키í…처 ì˜ì—­: í´ëž˜ìŠ¤ë¥¼ 아키í…처ì ìœ¼ë¡œ 나눈 ë ˆì´ì–´ë¥¼ ì˜ë¯¸í•¨(예: controller, service, domain, repository)
          +
          +---
          +
          +3) 테스트 피ë¼ë¯¸ë“œ
          +
          +- 단위 테스트 70%, 통합 테스트 20%, E2E 테스트 10%ì˜ ë¹„ìœ¨ë¡œ 권장
          +- Mike Cohnì´ "Succeeding with Agile"ì—서 ì²˜ìŒ ì œì‹œí•œ ê°œë…
          +- 단위 테스트ì—서 E2E 테스트로 가면서 ì†ë„는 ëŠë ¤ì§€ê³  ë¹„ìš©ì€ ë†’ì•„ì§
          +
          +---
          +
          +4) Red-Green-Refactor 사ì´í´
          +
          +Red-Green-Refactor는 TDD(Test-Driven Development)를 수행하는 핵심 사ì´í´ìž„
          +- Red (실패하는 테스트 작성)
          +    - 새로운 ê¸°ëŠ¥ì— ëŒ€í•œ 테스트 코드를 먼저 작성
          +    - ì•„ì§ êµ¬í˜„ì´ ì—†ìœ¼ë¯€ë¡œ 테스트는 실패
          +    - ì´ ë‹¨ê³„ì—서 ê¸°ëŠ¥ì˜ ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 설계
          +- Green (테스트 통과하는 코드 작성)
          +    - 테스트를 통과하는 ìµœì†Œí•œì˜ ì½”ë“œ 작성
          +    - 품질보다는 ë™ìž‘ì— ì´ˆì 
          +- Refactor (리팩토ë§)
          +    - 중복 제거, ê°€ë…성 개선
          +    - 테스트는 ê³„ì† í†µê³¼í•˜ë„ë¡ ìœ ì§€
          +    - 코드 품질 개선
          +
          +---
          +2. 테스트 전략
          +
          +1) 테스트 수행 ì›ì¹™: FIRST ì›ì¹™
          +- Fast: 테스트는 빠르게 실행ë˜ì–´ì•¼ 함
          +- Isolated: ê° í…ŒìŠ¤íŠ¸ëŠ” ë…립ì ì´ì–´ì•¼ 함
          +- Repeatable: ì–´ë–¤ 환경ì—ì„œë„ ë™ì¼í•œ 결과가 나와야 함
          +- Self-validating: 테스트는 성공/실패가 명확해야 함
          +- Timely: 테스트는 실제 코드 작성 ì „/ì§í›„ì— ìž‘ì„±ë˜ì–´ì•¼ 함
          +
          +---
          +
          +2) 공통 전략: 테스트 코드 작성 관련
          +- 한 테스트는 한 가지만 테스트
          +- Given-When-Then 패턴 사용
          +    - Given(준비): í…ŒìŠ¤íŠ¸ì— í•„ìš”í•œ ìƒíƒœì™€ ë°ì´í„°ë¥¼ 설정
          +    - When(실행): 테스트하려는 ë™ìž‘ì„ ìˆ˜í–‰
          +    - Then(ê²€ì¦): 기대하는 결과가 나왔는지 확ì¸
          +- 깨ë—한 테스트 코드 작성
          +    - 테스트 ì˜ë„를 명확히 하는 네ì´ë°
          +    - 테스트 ì¼€ì´ìŠ¤ëŠ” 시나리오 중심으로 구성
          +    - 공통 ì„¤ì •ì€ ë³„ë„ ë©”ì„œë“œë¡œ 분리
          +    - 매ì§ë„˜ë²„ 대신 ìƒìˆ˜ 사용
          +    - 테스트 ë°ì´í„°ëŠ” 최소한으로 사용
          +- 경계값 테스트가 중요
          +    - null ê°’
          +    - 빈 컬렉션
          +    - 최대/최소값
          +    - 0ì´ë‚˜ 1ê³¼ ê°™ì€ íŠ¹ìˆ˜ê°’
          +    - ìž˜ëª»ëœ í¬ë§·ì˜ 입력값
          +
          +---
          +
          +2) 공통 전략: 테스트 코드 관리 관련
          +- 비용 효율ì ì¸ 테스트 ì „ëžµ
          +    - ìžì£¼ 변경ë˜ëŠ” 비즈니스 로ì§ì— 대한 테스트 ê°•í™”
          +    - 실제 ìš´ì˜ í™˜ê²½ê³¼ 유사한 통합 테스트 구성
          +    - 테스트 실행 시간과 리소스 사용량 모니터ë§
          +- ì§€ì†ì ì¸ 테스트 개선
          +    - 테스트 커버리지보다 테스트 품질 중시
          +    - 깨진 테스트는 즉시 수정하는 문화 정착
          +    - 테스트 ì½”ë“œë„ ì‹¤ì œ ì½”ë“œë§Œí¼ ì¤‘ìš”í•˜ê²Œ 관리
          +- 팀 í˜‘ì—…ì„ ìœ„í•œ ê°€ì´ë“œë¼ì¸ 수립
          +    - 테스트 네ì´ë° 컨벤션 수립
          +    - 테스트 ë°ì´í„° 관리 ì „ëžµ í•©ì˜
          +    - 테스트 실패 시 ëŒ€ì‘ í”„ë¡œì„¸ìŠ¤ 수립
          +
          +---
          +
          +3) 단위 테스트 전략
          +- 테스트 범위 명확화
          +    - í´ëž˜ìŠ¤ì˜ ê° public 메소드가 수행하는 ë‹¨ì¼ ì±…ìž„ì„ ê²€ì¦
          +    - private 메서드는 public 메서드를 통해 ê°„ì ‘ì ìœ¼ë¡œ 테스트
          +- 외부 ì˜ì¡´ì„± 처리
          +    - DB, 파ì¼, ë„¤íŠ¸ì›Œí¬ ë“± 외부 ì‹œìŠ¤í…œì€ ê°€ì§œ ê°ì²´ë¡œ 대체(Mocking)
          +    - 테스트 ë”블(ìŠ¤í„´íŠ¸ë§¨ì„ Stunt Doubleì´ë¼ê³  함. 대역으로 ì´í•´)ì€ ê¼­ 필요한 ë™ìž‘ë§Œ 구현
          +        - Mock: 메소드 호출 여부와 파ë¼ë¯¸í„° ê²€ì¦
          +        - Stub: ë°˜í™˜ê°’ì˜ ì¼ì¹˜ 여부 ê²€ì¦
          +        - Spy: Mocking하지 않고 실제 메소드를 ê°ì‹¸ì„œ 호출횟수, 호출순서등 추가 ì •ë³´ ê²€ì¦
          +- 격리성 확보
          +    - 테스트 ê°„ ìƒí˜¸ ì˜í–¥ ì—†ë„ë¡ ì„¤ê³„: ë™ì¼ 공유 ìžì›/ê°ì²´ë¥¼ 사용하지 않게 함
          +    - 테스트 실행 순서와 무관하게 ë™ìž‘
          +- ê°€ë…성과 유지보수성
          +    - 테스트 ëŒ€ìƒ í´ëž˜ìŠ¤ë‹¹ í•˜ë‚˜ì˜ í…ŒìŠ¤íŠ¸ í´ëž˜ìФ
          +    - 테스트 메서드는 한 가지 시나리오만 ê²€ì¦
          +
          +---
          +
          +4) 단위 테스트 시 Mocking 전략
          +- 외부 시스템(DB, 외부 API 등)ì€ ë°˜ë“œì‹œ Mocking
          +- ê°™ì€ ë ˆì´ì–´ì˜ ì˜ì¡´ì„± 있는 í´ëž˜ìŠ¤ëŠ” 실제 ê°ì²´ 사용
          +- 예외ì ìœ¼ë¡œ ì˜ì¡´ ê°ì²´ê°€ 매우 복잡하거나 무거운 경우 Mocking ê³ ë ¤
          +
          +* 참고: ëª¨ì˜ ê°ì²´ 테스트 ê· í˜•ì  ì°¾ê¸°  
          +  출처: When to mocking by Uncle Bob(https://blog.cleancoder.com/uncle-bob/2014/05/10/WhenToMock.html)
          +- ëª¨ì˜ ê°ì²´ë¥¼ ì´ìš© 안 하면: 테스트가 오래 걸리고 결과를 신뢰하기 어려우며 ì¸í”„ë¼ì— 너무 ë§Žì€ ì˜í–¥ì„ ë°›ìŒ
          +- ëª¨ì˜ ê°ì²´ë¥¼ 지나치게 사용하면: 복잡하고 ìˆ˜ì •ì— ì˜í–¥ì„ 너무 ë§Žì´ ë°›ìœ¼ë©° ëª¨ì˜ ì¸í„°íŽ˜ì´ìŠ¤ê°€ í­ë°œì ìœ¼ë¡œ ì¦ê°€
          +- ê· í˜•ì  ì°¾ê¸°
          +    - 아키í…처ì ìœ¼ë¡œ 중요한 경계ì—서만 ëª¨ì˜ í…ŒìŠ¤íŠ¸ë¥¼ 수행하고, ê·¸ 경계 안ì—서는 하지 않는다.  
          +      (Mock across architecturally significant boundaries, but not within those boundaries.)
          +    - 여기서 경계란 Controller, Service, Repository, Domainë“±ì˜ ë ˆì´ì–´ë¥¼ ì˜ë¯¸í•¨
          +
          +---
          +5) 통합 테스트 전략
          +- 웹 서버 ì¸í„°íŽ˜ì´ìФ
          +    - @WebMvcTest, @WebFluxTest 활용
          +    - Controller ê³„ì¸µì˜ ìš”ì²­/ì‘답 ê²€ì¦
          +    - Service ê³„ì¸µì€ Mocking 처리
          +
          +- Database ì¸í„°íŽ˜ì´ìФ
          +    - @DataJpaTest 활용
          +    - TestContainer로 실제 DB 엔진 실행
          +
          +- 외부 서비스 ì¸í„°íŽ˜ì´ìФ
          +    - WireMock ë“±ì„ í™œìš©í•œ Mocking
          +    - 실제 API 스펙 기반 테스트
          +
          +- 테스트 환경 구성
          +    - 테스트용 ë³„ë„ ì„¤ì • íŒŒì¼ êµ¬ì„±
          +    - 테스트 ë°ì´í„°ëŠ” 테스트 시작 시 초기화
          +    - @Transactionalì„ í™œìš©í•œ 테스트 격리
          +    - 테스트 ê°„ ë…립성 보장
          +
          +---
          +6) E2E 테스트 전략
          +- ì›ì¹™
          +    - 단위 테스트나 ì»´í¬ë„ŒíЏ 테스트ì—서 놓칠 수 있는 시나리오를 찾아내는 ê²ƒì´ ëª©í‘œìž„
          +    - 조건별 로ì§ì´ë‚˜ 분기 ìƒí™©(edge cases)ì´ ì•„ë‹Œ ìƒìœ„ ìˆ˜ì¤€ì˜ ì¼ë°˜ì ì¸ 시나리오만 테스트
          +    - 만약 ì–´ë–¤ 시스템 테스트 시나리오가 실패 í–ˆëŠ”ë° ë‹¨ìœ„ 테스트나 통합 테스트가 없다면 만들어야 함
          +
          +- ìš´ì˜ê³¼ ë™ì¼í•œ 테스트 환경 구성: 웹서버/WAS, DB, ìºì‹œ, MQ, 외부시스템
          +- 테스트 ë°ì´í„° 관리
          +    - 테스트용 마스터 ë°ì´í„° 구성
          +    - 시나리오별 테스트 ë°ì´í„° 세트 준비
          +    - ë°ì´í„° 초기화 ë° ì •ë¦¬ ìžë™í™”
          +- 테스트 ìžë™í™” ì „ëžµ
          +    - UI 테스트: Selenium, Cucumber, Playwright 등 ë„구 활용
          +    - API 테스트: Rest-Assured, Postman 등 ë„구 활용
          +
          +---
          +
          +7) 테스트 코드 네ì´ë° 컨벤션
          +
          +- 패키지 네ì´ë°
          +```
          +[Syntax]
          +{프로ë•션패키지}.test.{테스트유형}
          +
          +[Example]
          +- 단위테스트: com.company.order.test.unit
          +- 통합테스트: com.company.order.test.integration
          +- E2E테스트: com.company.order.test.e2e
          +```
          +
          +- í´ëž˜ìФ 네ì´ë°
          +```
          +[Syntax]
          +{대ìƒí´ëž˜ìФ}{테스트유형}Test
          +
          +[Example]
          +- 단위테스트: OrderServiceUnitTest
          +- 통합테스트: OrderServiceIntegrationTest
          +- E2E테스트: OrderServiceE2ETest
          +```
          +
          +- 메소드 네ì´ë°
          +```
          +[Syntax]
          +given{초기ìƒíƒœ}_when{행위}_then{ê²°ê³¼}
          +
          +[Example]
          +givenEmptyCart_whenAddItem_thenSuccess()
          +givenInvalidToken_whenAuthenticate_thenThrowException()
          +givenExistingUser_whenUpdateProfile_thenProfileUpdated()
          +```
          +
          +- 테스트 ë°ì´í„° 네ì´ë°
          +```
          +[Syntax]
          +ìƒìˆ˜: {ìƒíƒœ}_{대ìƒ}
          +변수: {ìƒíƒœ}{대ìƒ}
          +
          +[Example]
          +// ìƒìˆ˜
          +VALID_USER_ID = 1L
          +EMPTY_ORDER_LIST = Collections.emptyList()
          +
          +// 변수
          +normalUser = new User(...)
          +emptyCart = new Cart()
          +```
          
          From 7a99dc95fe9c45880cfa08414590cee3972a4a12 Mon Sep 17 00:00:00 2001
          From: Unknown 
          Date: Tue, 28 Oct 2025 10:21:38 +0900
          Subject: [PATCH 14/61] =?UTF-8?q?participation=20=EC=8B=A4=ED=96=89?=
           =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?=
          MIME-Version: 1.0
          Content-Type: text/plain; charset=UTF-8
          Content-Transfer-Encoding: 8bit
          
          ---
           .run/ParticipationServiceApplication.run.xml       |  2 +-
           .../.run/participation-service.run.xml             |  6 +++---
           participation-service/fix-indexes.sql              | 14 ++++++++++++++
           .../event/participation/domain/draw/DrawLog.java   |  2 +-
           .../domain/participant/Participant.java            |  4 ++--
           .../src/main/resources/application.yml             |  2 +-
           6 files changed, 22 insertions(+), 8 deletions(-)
           create mode 100644 participation-service/fix-indexes.sql
          
          diff --git a/.run/ParticipationServiceApplication.run.xml b/.run/ParticipationServiceApplication.run.xml
          index a323100..8102290 100644
          --- a/.run/ParticipationServiceApplication.run.xml
          +++ b/.run/ParticipationServiceApplication.run.xml
          @@ -43,7 +43,7 @@