From 353e1b114770ebd092011aa710a02f47cb5134fe Mon Sep 17 00:00:00 2001 From: yeonjy <81320703+yeonjy@users.noreply.github.com> Date: Sat, 5 Apr 2025 01:00:17 +0900 Subject: [PATCH 001/309] Update issue templates --- ...0-\355\205\234\355\224\214\353\246\277.md" | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 ".github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" diff --git "a/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" "b/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" new file mode 100644 index 0000000..0d8cc9f --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" @@ -0,0 +1,27 @@ +--- +name: 이슈 템플릿 +about: 해당 이슈 템플릿을 사용하여 이슈를 생성해주세요. +title: '' +labels: '' +assignees: '' + +--- + +--- +name: issue_template +about: issue template +title: '' +labels: '' +assignees: '' + +--- + +## 어떤 기능인가요? + +> 추가하려는 기능에 대해 간결하게 설명해주세요 + +## 작업 상세 내용 + +- [ ] TODO +- [ ] TODO +- [ ] TODO From 250066fdced13aa4848c40022e89784abdf34ee1 Mon Sep 17 00:00:00 2001 From: yeonjy <81320703+yeonjy@users.noreply.github.com> Date: Sat, 5 Apr 2025 01:01:43 +0900 Subject: [PATCH 002/309] Create pull_request_template.md --- .github/pull_request_template.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..75ff8d0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +## #️⃣연관된 이슈 +> ex) #이슈번호, #이슈번호 + +## 📝작업 내용 +> 이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능) + +### 스크린샷 (선택) + +## 💬리뷰 요구사항(선택) +> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요 +> +> ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요? From a6a7631f1d28883662f61c8798a21f0b21e6b1b2 Mon Sep 17 00:00:00 2001 From: yeonjy <81320703+yeonjy@users.noreply.github.com> Date: Sat, 5 Apr 2025 01:23:31 +0900 Subject: [PATCH 003/309] Update issue templates --- .../ISSUE_TEMPLATE/issue_template.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) rename ".github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" => .github/ISSUE_TEMPLATE/issue_template.md (76%) diff --git "a/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" b/.github/ISSUE_TEMPLATE/issue_template.md similarity index 76% rename from ".github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" rename to .github/ISSUE_TEMPLATE/issue_template.md index 0d8cc9f..c9af9b8 100644 --- "a/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -1,15 +1,6 @@ ---- -name: 이슈 템플릿 -about: 해당 이슈 템플릿을 사용하여 이슈를 생성해주세요. -title: '' -labels: '' -assignees: '' - ---- - --- name: issue_template -about: issue template +about: 해당 이슈 템플릿을 사용하여 이슈를 생성해주세요. title: '' labels: '' assignees: '' From f51fe5c74b87e403015df1a87bd9ed7471c511ab Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 5 Apr 2025 01:28:12 +0900 Subject: [PATCH 004/309] feat: (#1) project setup --- .gitattributes | 3 + .gitignore | 48 ++++ build.gradle | 39 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 251 ++++++++++++++++++ gradlew.bat | 94 +++++++ settings.gradle | 1 + .../webeye/backend/BackendApplication.java | 13 + src/main/resources/application.yml | 1 + .../backend/BackendApplicationTests.java | 13 + 11 files changed, 470 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/webeye/backend/BackendApplication.java create mode 100644 src/main/resources/application.yml create mode 100644 src/test/java/com/webeye/backend/BackendApplicationTests.java diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..606d42e --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +### Gradle ### +.gradle +**/build/ +!src/**/build/ \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3a857e0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.10' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.webeye' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 zcma&Obx`DOvL%eWOXJW;V64viP??$)@wHcsJ68)>bJS6*&iHnskXE8MjvIPVl|FrmV}Npeql07fCw6`pw`0s zGauF(<*@v{3t!qoUU*=j)6;|-(yg@jvDx&fV^trtZt27?4Tkn729qrItVh@PMwG5$ z+oXHSPM??iHZ!cVP~gYact-CwV`}~Q+R}PPNRy+T-geK+>fHrijpllon_F4N{@b-} z1M0=a!VbVmJM8Xk@NRv)m&aRYN}FSJ{LS;}2ArQ5baSjfy40l@T5)1r-^0fAU6f_} zzScst%$Nd-^ElV~H0TetQhMc%S{}Q4lssln=|;LG?Ulo}*mhg8YvBAUY7YFdXs~vv zv~{duzVw%C#GxkBwX=TYp1Dh*Uaum2?RmsvPaLlzO^fIJ`L?&OV?Y&kKj~^kWC`Ly zfL-}J^4a0Ojuz9O{jUbIS;^JatJ5+YNNHe}6nG9Yd6P-lJiK2ms)A^xq^H2fKrTF) zp!6=`Ece~57>^9(RA4OB9;f1FAhV%zVss%#rDq$9ZW3N2cXC7dMz;|UcRFecBm`DA z1pCO!#6zKp#@mx{2>Qcme8y$Qg_gnA%(`Vtg3ccwgb~D(&@y8#Jg8nNYW*-P{_M#E zZ|wCsQoO1(iIKd-2B9xzI}?l#Q@G5d$m1Lfh0q;iS5FDQ&9_2X-H)VDKA*fa{b(sV zL--krNCXibi1+*C2;4qVjb0KWUVGjjRT{A}Q*!cFmj0tRip2ra>WYJ>ZK4C|V~RYs z6;~+*)5F^x^aQqk9tjh)L;DOLlD8j+0<>kHc8MN|68PxQV`tJFbgxSfq-}b(_h`luA0&;Vk<@51i0 z_cu6{_*=vlvYbKjDawLw+t^H?OV00_73Cn3goU5?})UYFuoSX6Xqw;TKcrsc|r# z$sMWYl@cs#SVopO$hpHZ)cdU-+Ui%z&Sa#lMI~zWW@vE%QDh@bTe0&V9nL>4Et9`N zGT8(X{l@A~loDx}BDz`m6@tLv@$mTlVJ;4MGuj!;9Y=%;;_kj#o8n5tX%@M)2I@}u z_{I!^7N1BxW9`g&Z+K#lZ@7_dXdsqp{W9_`)zgZ=sD~%WS5s$`7z#XR!Lfy(4se(m zR@a3twgMs19!-c4jh`PfpJOSU;vShBKD|I0@rmv_x|+ogqslnLLOepJpPMOxhRb*i zGHkwf#?ylQ@k9QJL?!}MY4i7joSzMcEhrDKJH&?2v{-tgCqJe+Y0njl7HYff z{&~M;JUXVR$qM1FPucIEY(IBAuCHC@^~QG6O!dAjzQBxDOR~lJEr4KS9R*idQ^p{D zS#%NQADGbAH~6wAt}(1=Uff-1O#ITe)31zCL$e9~{w)gx)g>?zFE{Bc9nJT6xR!i8 z)l)~9&~zSZTHk{?iQL^MQo$wLi}`B*qnvUy+Y*jEraZMnEhuj`Fu+>b5xD1_Tp z)8|wedv42#3AZUL7x&G@p@&zcUvPkvg=YJS6?1B7ZEXr4b>M+9Gli$gK-Sgh{O@>q7TUg+H zNJj`6q#O@>4HpPJEHvNij`sYW&u%#=215HKNg;C!0#hH1vlO5+dFq9& zS)8{5_%hz?#D#wn&nm@aB?1_|@kpA@{%jYcs{K%$a4W{k@F zPyTav?jb;F(|GaZhm6&M#g|`ckO+|mCtAU)5_(hn&Ogd z9Ku}orOMu@K^Ac>eRh3+0-y^F`j^noa*OkS3p^tLV`TY$F$cPXZJ48!xz1d7%vfA( zUx2+sDPqHfiD-_wJDb38K^LtpN2B0w=$A10z%F9f_P2aDX63w7zDG5CekVQJGy18I zB!tI`6rZr7TK10L(8bpiaQ>S@b7r_u@lh^vakd0e6USWw7W%d_Ob%M!a`K>#I3r-w zo2^+9Y)Sb?P9)x0iA#^ns+Kp{JFF|$09jb6ZS2}_<-=$?^#IUo5;g`4ICZknr!_aJ zd73%QP^e-$%Xjt|28xM}ftD|V@76V_qvNu#?Mt*A-OV{E4_zC4Ymo|(cb+w^`Wv== z>)c%_U0w`d$^`lZQp@midD89ta_qTJW~5lRrIVwjRG_9aRiQGug%f3p@;*%Y@J5uQ|#dJ+P{Omc`d2VR)DXM*=ukjVqIpkb<9gn9{*+&#p)Ek zN=4zwNWHF~=GqcLkd!q0p(S2_K=Q`$whZ}r@ec_cb9hhg9a z6CE=1n8Q;hC?;ujo0numJBSYY6)GTq^=kB~`-qE*h%*V6-ip=c4+Yqs*7C@@b4YAi zuLjsmD!5M7r7d5ZPe>4$;iv|zq=9=;B$lI|xuAJwi~j~^Wuv!Qj2iEPWjh9Z&#+G>lZQpZ@(xfBrhc{rlLwOC;optJZDj4Xfu3$u6rt_=YY0~lxoy~fq=*L_&RmD7dZWBUmY&12S;(Ui^y zBpHR0?Gk|`U&CooNm_(kkO~pK+cC%uVh^cnNn)MZjF@l{_bvn4`Jc}8QwC5_)k$zs zM2qW1Zda%bIgY^3NcfL)9ug`05r5c%8ck)J6{fluBQhVE>h+IA&Kb}~$55m-^c1S3 zJMXGlOk+01qTQUFlh5Jc3xq|7McY$nCs$5=`8Y;|il#Ypb{O9}GJZD8!kYh{TKqs@ z-mQn1K4q$yGeyMcryHQgD6Ra<6^5V(>6_qg`3uxbl|T&cJVA*M_+OC#>w(xL`RoPQ zf1ZCI3G%;o-x>RzO!mc}K!XX{1rih0$~9XeczHgHdPfL}4IPi~5EV#ZcT9 zdgkB3+NPbybS-d;{8%bZW^U+x@Ak+uw;a5JrZH!WbNvl!b~r4*vs#he^bqz`W93PkZna2oYO9dBrKh2QCWt{dGOw)%Su%1bIjtp4dKjZ^ zWfhb$M0MQiDa4)9rkip9DaH0_tv=XxNm>6MKeWv>`KNk@QVkp$Lhq_~>M6S$oliq2 zU6i7bK;TY)m>-}X7hDTie>cc$J|`*}t=MAMfWIALRh2=O{L57{#fA_9LMnrV(HrN6 zG0K_P5^#$eKt{J|#l~U0WN_3)p^LLY(XEqes0OvI?3)GTNY&S13X+9`6PLVFRf8K) z9x@c|2T72+-KOm|kZ@j4EDDec>03FdgQlJ!&FbUQQH+nU^=U3Jyrgu97&#-W4C*;_ z(WacjhBDp@&Yon<9(BWPb;Q?Kc0gR5ZH~aRNkPAWbDY!FiYVSu!~Ss^9067|JCrZk z-{Rn2KEBR|Wti_iy) zXnh2wiU5Yz2L!W{{_#LwNWXeNPHkF=jjXmHC@n*oiz zIoM~Wvo^T@@t!QQW?Ujql-GBOlnB|HjN@x~K8z)c(X}%%5Zcux09vC8=@tvgY>czq z3D(U&FiETaN9aP}FDP3ZSIXIffq>M3{~eTB{uauL07oYiM=~K(XA{SN!rJLyXeC+Y zOdeebgHOc2aCIgC=8>-Q>zfuXV*=a&gp{l#E@K|{qft@YtO>xaF>O7sZz%8);e86? z+jJlFB{0fu6%8ew^_<+v>>%6eB8|t*_v7gb{x=vLLQYJKo;p7^o9!9A1)fZZ8i#ZU z<|E?bZakjkEV8xGi?n+{Xh3EgFKdM^;4D;5fHmc04PI>6oU>>WuLy6jgpPhf8$K4M zjJo*MbN0rZbZ!5DmoC^@hbqXiP^1l7I5;Wtp2i9Jkh+KtDJoXP0O8qmN;Sp(+%upX zAxXs*qlr(ck+-QG_mMx?hQNXVV~LT{$Q$ShX+&x?Q7v z@8t|UDylH6@RZ?WsMVd3B0z5zf50BP6U<&X_}+y3uJ0c5OD}+J&2T8}A%2Hu#Nt_4 zoOoTI$A!hQ<2pk5wfZDv+7Z{yo+Etqry=$!*pvYyS+kA4xnJ~3b~TBmA8Qd){w_bE zqDaLIjnU8m$wG#&T!}{e0qmHHipA{$j`%KN{&#_Kmjd&#X-hQN+ju$5Ms$iHj4r?) z&5m8tI}L$ih&95AjQ9EDfPKSmMj-@j?Q+h~C3<|Lg2zVtfKz=ft{YaQ1i6Om&EMll zzov%MsjSg=u^%EfnO+W}@)O6u0LwoX709h3Cxdc2Rwgjd%LLTChQvHZ+y<1q6kbJXj3_pq1&MBE{8 zd;aFotyW>4WHB{JSD8Z9M@jBitC1RF;!B8;Rf-B4nOiVbGlh9w51(8WjL&e{_iXN( zAvuMDIm_>L?rJPxc>S`bqC|W$njA0MKWa?V$u6mN@PLKYqak!bR!b%c^ze(M`ec(x zv500337YCT4gO3+9>oVIJLv$pkf`01S(DUM+4u!HQob|IFHJHm#>eb#eB1X5;bMc| z>QA4Zv}$S?fWg~31?Lr(C>MKhZg>gplRm`2WZ--iw%&&YlneQYY|PXl;_4*>vkp;I z$VYTZq|B*(3(y17#@ud@o)XUZPYN*rStQg5U1Sm2gM}7hf_G<>*T%6ebK*tF(kbJc zNPH4*xMnJNgw!ff{YXrhL&V$6`ylY={qT_xg9znQWw9>PlG~IbhnpsG_94Kk_(V-o&v7#F znra%uD-}KOX2dkak**hJnZZQyp#ERyyV^lNe!Qrg=VHiyr7*%j#PMvZMuYNE8o;JM zGrnDWmGGy)(UX{rLzJ*QEBd(VwMBXnJ@>*F8eOFy|FK*Vi0tYDw;#E zu#6eS;%Nm2KY+7dHGT3m{TM7sl=z8|V0e!DzEkY-RG8vTWDdSQFE|?+&FYA146@|y zV(JP>LWL;TSL6rao@W5fWqM1-xr$gRci#RQV2DX-x4@`w{uEUgoH4G|`J%H!N?*Qn zy~rjzuf(E7E!A9R2bSF|{{U(zO+;e29K_dGmC^p7MCP!=Bzq@}&AdF5=rtCwka zTT1A?5o}i*sXCsRXBt)`?nOL$zxuP3i*rm3Gmbmr6}9HCLvL*45d|(zP;q&(v%}S5yBmRVdYQQ24zh z6qL2<2>StU$_Ft29IyF!6=!@;tW=o8vNzVy*hh}XhZhUbxa&;9~woye<_YmkUZ)S?PW{7t; zmr%({tBlRLx=ffLd60`e{PQR3NUniWN2W^~7Sy~MPJ>A#!6PLnlw7O0(`=PgA}JLZ ztqhiNcKvobCcBel2 z-N82?4-()eGOisnWcQ9Wp23|ybG?*g!2j#>m3~0__IX1o%dG4b;VF@^B+mRgKx|ij zWr5G4jiRy}5n*(qu!W`y54Y*t8g`$YrjSunUmOsqykYB4-D(*(A~?QpuFWh;)A;5= zPl|=x+-w&H9B7EZGjUMqXT}MkcSfF}bHeRFLttu!vHD{Aq)3HVhvtZY^&-lxYb2%` zDXk7>V#WzPfJs6u{?ZhXpsMdm3kZscOc<^P&e&684Rc1-d=+=VOB)NR;{?0NjTl~D z1MXak$#X4{VNJyD$b;U~Q@;zlGoPc@ny!u7Pe;N2l4;i8Q=8>R3H{>HU(z z%hV2?rSinAg6&wuv1DmXok`5@a3@H0BrqsF~L$pRYHNEXXuRIWom0l zR9hrZpn1LoYc+G@q@VsFyMDNX;>_Vf%4>6$Y@j;KSK#g)TZRmjJxB!_NmUMTY(cAV zmewn7H{z`M3^Z& z2O$pWlDuZHAQJ{xjA}B;fuojAj8WxhO}_9>qd0|p0nBXS6IIRMX|8Qa!YDD{9NYYK z%JZrk2!Ss(Ra@NRW<7U#%8SZdWMFDU@;q<}%F{|6n#Y|?FaBgV$7!@|=NSVoxlJI4G-G(rn}bh|?mKkaBF$-Yr zA;t0r?^5Nz;u6gwxURapQ0$(-su(S+24Ffmx-aP(@8d>GhMtC5x*iEXIKthE*mk$` zOj!Uri|EAb4>03C1xaC#(q_I<;t}U7;1JqISVHz3tO{) zD(Yu@=>I9FDmDtUiWt81;BeaU{_=es^#QI7>uYl@e$$lGeZ~Q(f$?^3>$<<{n`Bn$ zn8bamZlL@6r^RZHV_c5WV7m2(G6X|OI!+04eAnNA5=0v1Z3lxml2#p~Zo57ri;4>;#16sSXXEK#QlH>=b$inEH0`G#<_ zvp;{+iY)BgX$R!`HmB{S&1TrS=V;*5SB$7*&%4rf_2wQS2ed2E%Wtz@y$4ecq4w<) z-?1vz_&u>s?BMrCQG6t9;t&gvYz;@K@$k!Zi=`tgpw*v-#U1Pxy%S9%52`uf$XMv~ zU}7FR5L4F<#9i%$P=t29nX9VBVv)-y7S$ZW;gmMVBvT$BT8d}B#XV^@;wXErJ-W2A zA=JftQRL>vNO(!n4mcd3O27bHYZD!a0kI)6b4hzzL9)l-OqWn)a~{VP;=Uo|D~?AY z#8grAAASNOkFMbRDdlqVUfB;GIS-B-_YXNlT_8~a|LvRMVXf!<^uy;)d$^OR(u)!) zHHH=FqJF-*BXif9uP~`SXlt0pYx|W&7jQnCbjy|8b-i>NWb@!6bx;1L&$v&+!%9BZ z0nN-l`&}xvv|wwxmC-ZmoFT_B#BzgQZxtm|4N+|;+(YW&Jtj^g!)iqPG++Z%x0LmqnF875%Ry&2QcCamx!T@FgE@H zN39P6e#I5y6Yl&K4eUP{^biV`u9{&CiCG#U6xgGRQr)zew;Z%x+ z-gC>y%gvx|dM=OrO`N@P+h2klPtbYvjS!mNnk4yE0+I&YrSRi?F^plh}hIp_+OKd#o7ID;b;%*c0ES z!J))9D&YufGIvNVwT|qsGWiZAwFODugFQ$VsNS%gMi8OJ#i${a4!E3<-4Jj<9SdSY z&xe|D0V1c`dZv+$8>(}RE|zL{E3 z-$5Anhp#7}oO(xm#}tF+W=KE*3(xxKxhBt-uuJP}`_K#0A< zE%rhMg?=b$ot^i@BhE3&)bNBpt1V*O`g?8hhcsV-n#=|9wGCOYt8`^#T&H7{U`yt2 z{l9Xl5CVsE=`)w4A^%PbIR6uG_5Ww9k`=q<@t9Bu662;o{8PTjDBzzbY#tL;$wrpjONqZ{^Ds4oanFm~uyPm#y1Ll3(H57YDWk9TlC zq;kebC!e=`FU&q2ojmz~GeLxaJHfs0#F%c(i+~gg$#$XOHIi@1mA72g2pFEdZSvp}m0zgQb5u2?tSRp#oo!bp`FP}< zaK4iuMpH+Jg{bb7n9N6eR*NZfgL7QiLxI zk6{uKr>xxJ42sR%bJ%m8QgrL|fzo9@?9eQiMW8O`j3teoO_R8cXPe_XiLnlYkE3U4 zN!^F)Z4ZWcA8gekEPLtFqX-Q~)te`LZnJK_pgdKs)Dp50 zdUq)JjlJeELskKg^6KY!sIou-HUnSFRsqG^lsHuRs`Z{f(Ti9eyd3cwu*Kxp?Ws7l z3cN>hGPXTnQK@qBgqz(n*qdJ2wbafELi?b90fK~+#XIkFGU4+HihnWq;{{)1J zv*Txl@GlnIMOjzjA1z%g?GsB2(6Zb-8fooT*8b0KF2CdsIw}~Hir$d3TdVHRx1m3c z4C3#h@1Xi@{t4zge-#B6jo*ChO%s-R%+9%-E|y<*4;L>$766RiygaLR?X%izyqMXA zb|N=Z-0PSFeH;W6aQ3(5VZWVC>5Ibgi&cj*c%_3=o#VyUJv* zM&bjyFOzlaFq;ZW(q?|yyi|_zS%oIuH^T*MZ6NNXBj;&yM3eQ7!CqXY?`7+*+GN47 zNR#%*ZH<^x{(0@hS8l{seisY~IE*)BD+R6^OJX}<2HRzo^fC$n>#yTOAZbk4%=Bei=JEe=o$jm`or0YDw*G?d> z=i$eEL7^}_?UI^9$;1Tn9b>$KOM@NAnvWrcru)r`?LodV%lz55O3y(%FqN;cKgj7t zlJ7BmLTQ*NDX#uelGbCY>k+&H*iSK?x-{w;f5G%%!^e4QT9z<_0vHbXW^MLR} zeC*jezrU|{*_F`I0mi)9=sUj^G03i@MjXx@ePv@(Udt2CCXVOJhRh4yp~fpn>ssHZ z?k(C>2uOMWKW5FVsBo#Nk!oqYbL`?#i~#!{3w^qmCto05uS|hKkT+iPrC-}hU_nbL zO622#mJupB21nChpime}&M1+whF2XM?prT-Vv)|EjWYK(yGYwJLRRMCkx;nMSpu?0 zNwa*{0n+Yg6=SR3-S&;vq=-lRqN`s9~#)OOaIcy3GZ&~l4g@2h| zThAN#=dh{3UN7Xil;nb8@%)wx5t!l z0RSe_yJQ+_y#qEYy$B)m2yDlul^|m9V2Ia$1CKi6Q19~GTbzqk*{y4;ew=_B4V8zw zScDH&QedBl&M*-S+bH}@IZUSkUfleyM45G>CnYY{hx8J9q}ME?Iv%XK`#DJRNmAYt zk2uY?A*uyBA=nlYjkcNPMGi*552=*Q>%l?gDK_XYh*Rya_c)ve{=ps`QYE0n!n!)_$TrGi_}J|>1v}(VE7I~aP-wns#?>Y zu+O7`5kq32zM4mAQpJ50vJsUDT_^s&^k-llQMy9!@wRnxw@~kXV6{;z_wLu3i=F3m z&eVsJmuauY)8(<=pNUM5!!fQ4uA6hBkJoElL1asWNkYE#qaP?a+biwWw~vB48PRS7 zY;DSHvgbIB$)!uJU)xA!yLE*kP0owzYo`v@wfdux#~f!dv#uNc_$SF@Qq9#3q5R zfuQnPPN_(z;#X#nRHTV>TWL_Q%}5N-a=PhkQ^GL+$=QYfoDr2JO-zo#j;mCsZVUQ) zJ96e^OqdLW6b-T@CW@eQg)EgIS9*k`xr$1yDa1NWqQ|gF^2pn#dP}3NjfRYx$pTrb zwGrf8=bQAjXx*8?du*?rlH2x~^pXjiEmj^XwQo{`NMonBN=Q@Y21!H)D( zA~%|VhiTjaRQ%|#Q9d*K4j~JDXOa4wmHb0L)hn*;Eq#*GI}@#ux4}bt+olS(M4$>c z=v8x74V_5~xH$sP+LZCTrMxi)VC%(Dg!2)KvW|Wwj@pwmH6%8zd*x0rUUe$e(Z%AW z@Q{4LL9#(A-9QaY2*+q8Yq2P`pbk3!V3mJkh3uH~uN)+p?67d(r|Vo0CebgR#u}i? zBxa^w%U|7QytN%L9bKaeYhwdg7(z=AoMeP0)M3XZA)NnyqL%D_x-(jXp&tp*`%Qsx z6}=lGr;^m1<{;e=QQZ!FNxvLcvJVGPkJ63at5%*`W?46!6|5FHYV0qhizSMT>Zoe8 zsJ48kb2@=*txGRe;?~KhZgr-ZZ&c0rNV7eK+h$I-UvQ=552@psVrvj#Ys@EU4p8`3 zsNqJu-o=#@9N!Pq`}<=|((u)>^r0k^*%r<{YTMm+mOPL>EoSREuQc-e2~C#ZQ&Xve zZ}OUzmE4{N-7cqhJiUoO_V#(nHX11fdfVZJT>|6CJGX5RQ+Ng$Nq9xs-C86-)~`>p zW--X53J`O~vS{WWjsAuGq{K#8f#2iz` zzSSNIf6;?5sXrHig%X(}0q^Y=eYwvh{TWK-fT>($8Ex>!vo_oGFw#ncr{vmERi^m7lRi%8Imph})ZopLoIWt*eFWSPuBK zu>;Pu2B#+e_W|IZ0_Q9E9(s@0>C*1ft`V{*UWz^K<0Ispxi@4umgGXW!j%7n+NC~* zBDhZ~k6sS44(G}*zg||X#9Weto;u*Ty;fP!+v*7be%cYG|yEOBomch#m8Np!Sw`L)q+T` zmrTMf2^}7j=RPwgpO9@eXfb{Q>GW#{X=+xt`AwTl!=TgYm)aS2x5*`FSUaaP_I{Xi zA#irF%G33Bw>t?^1YqX%czv|JF0+@Pzi%!KJ?z!u$A`Catug*tYPO`_Zho5iip0@! z;`rR0-|Ao!YUO3yaujlSQ+j-@*{m9dHLtve!sY1Xq_T2L3&=8N;n!!Eb8P0Z^p4PL zQDdZ?An2uzbIakOpC|d@=xEA}v-srucnX3Ym{~I#Ghl~JZU(a~Ppo9Gy1oZH&Wh%y zI=KH_s!Lm%lAY&`_KGm*Ht)j*C{-t}Nn71drvS!o|I|g>ZKjE3&Mq0TCs6}W;p>%M zQ(e!h*U~b;rsZ1OPigud>ej=&hRzs@b>>sq6@Yjhnw?M26YLnDH_Wt#*7S$-BtL08 zVyIKBm$}^vp?ILpIJetMkW1VtIc&7P3z0M|{y5gA!Yi5x4}UNz5C0Wdh02!h zNS>923}vrkzl07CX`hi)nj-B?#n?BJ2Vk0zOGsF<~{Fo7OMCN_85daxhk*pO}x_8;-h>}pcw26V6CqR-=x2vRL?GB#y%tYqi;J}kvxaz}*iFO6YO0ha6!fHU9#UI2Nv z_(`F#QU1B+P;E!t#Lb)^KaQYYSewj4L!_w$RH%@IL-M($?DV@lGj%3ZgVdHe^q>n(x zyd5PDpGbvR-&p*eU9$#e5#g3-W_Z@loCSz}f~{94>k6VRG`e5lI=SE0AJ7Z_+=nnE zTuHEW)W|a8{fJS>2TaX zuRoa=LCP~kP)kx4L+OqTjtJOtXiF=y;*eUFgCn^Y@`gtyp?n14PvWF=zhNGGsM{R- z^DsGxtoDtx+g^hZi@E2Y(msb-hm{dWiHdoQvdX88EdM>^DS#f}&kCGpPFDu*KjEpv$FZtLpeT>@)mf|z#ZWEsueeW~hF78Hu zfY9a+Gp?<)s{Poh_qdcSATV2oZJo$OH~K@QzE2kCADZ@xX(; z)0i=kcAi%nvlsYagvUp(z0>3`39iKG9WBDu3z)h38p|hLGdD+Khk394PF3qkX!02H z#rNE`T~P9vwNQ_pNe0toMCRCBHuJUmNUl)KFn6Gu2je+p>{<9^oZ4Gfb!)rLZ3CR3 z-o&b;Bh>51JOt=)$-9+Z!P}c@cKev_4F1ZZGs$I(A{*PoK!6j@ZJrAt zv2LxN#p1z2_0Ox|Q8PVblp9N${kXkpsNVa^tNWhof)8x8&VxywcJz#7&P&d8vvxn` zt75mu>yV=Dl#SuiV!^1BPh5R)`}k@Nr2+s8VGp?%Le>+fa{3&(XYi~{k{ z-u4#CgYIdhp~GxLC+_wT%I*)tm4=w;ErgmAt<5i6c~)7JD2olIaK8by{u-!tZWT#RQddptXRfEZxmfpt|@bs<*uh?Y_< zD>W09Iy4iM@@80&!e^~gj!N`3lZwosC!!ydvJtc0nH==K)v#ta_I}4Tar|;TLb|+) zSF(;=?$Z0?ZFdG6>Qz)6oPM}y1&zx_Mf`A&chb znSERvt9%wdPDBIU(07X+CY74u`J{@SSgesGy~)!Mqr#yV6$=w-dO;C`JDmv=YciTH zvcrN1kVvq|(3O)NNdth>X?ftc`W2X|FGnWV%s})+uV*bw>aoJ#0|$pIqK6K0Lw!@- z3pkPbzd`ljS=H2Bt0NYe)u+%kU%DWwWa>^vKo=lzDZHr>ruL5Ky&#q7davj-_$C6J z>V8D-XJ}0cL$8}Xud{T_{19#W5y}D9HT~$&YY-@=Th219U+#nT{tu=d|B)3K`pL53 zf7`I*|L@^dPEIDJkI3_oA9vsH7n7O}JaR{G~8 zfi$?kmKvu20(l`dV7=0S43VwVKvtF!7njv1Q{Ju#ysj=|dASq&iTE8ZTbd-iiu|2& zmll%Ee1|M?n9pf~?_tdQ<7%JA53!ulo1b^h#s|Su2S4r{TH7BRB3iIOiX5|vc^;5( zKfE1+ah18YA9o1EPT(AhBtve5(%GMbspXV)|1wf5VdvzeYt8GVGt0e*3|ELBhwRaO zE|yMhl;Bm?8Ju3-;DNnxM3Roelg`^!S%e({t)jvYtJCKPqN`LmMg^V&S z$9OIFLF$%Py~{l?#ReyMzpWixvm(n(Y^Am*#>atEZ8#YD&?>NUU=zLxOdSh0m6mL? z_twklB0SjM!3+7U^>-vV=KyQZI-6<(EZiwmNBzGy;Sjc#hQk%D;bay$v#zczt%mFCHL*817X4R;E$~N5(N$1Tv{VZh7d4mhu?HgkE>O+^-C*R@ zR0ima8PsEV*WFvz`NaB+lhX3&LUZcWWJJrG7ZjQrOWD%_jxv=)`cbCk zMgelcftZ%1-p9u!I-Zf_LLz{hcn5NRbxkWby@sj2XmYfAV?iw^0?hM<$&ZDctdC`; zsL|C-7d;w$z2Gt0@hsltNlytoPnK&$>ksr(=>!7}Vk#;)Hp)LuA7(2(Hh(y3LcxRY zim!`~j6`~B+sRBv4 z<#B{@38kH;sLB4eH2+8IPWklhd25r5j2VR}YK$lpZ%7eVF5CBr#~=kUp`i zlb+>Z%i%BJH}5dmfg1>h7U5Q(-F{1d=aHDbMv9TugohX5lq#szPAvPE|HaokMQIi_ zTcTNsO53(oX=hg2w!XA&+qP}nwr$(C)pgG8emS@Mf7m0&*kiA!wPLS`88c=aD$niJ zp?3j%NI^uy|5*MzF`k4hFbsyQZ@wu!*IY+U&&9PwumdmyfL(S0#!2RFfmtzD3m9V7 zsNOw9RQofl-XBfKBF^~~{oUVouka#r3EqRf=SnleD=r1Hm@~`y8U7R)w16fgHvK-6?-TFth)f3WlklbZh+}0 zx*}7oDF4U^1tX4^$qd%987I}g;+o0*$Gsd=J>~Uae~XY6UtbdF)J8TzJXoSrqHVC) zJ@pMgE#;zmuz?N2MIC+{&)tx=7A%$yq-{GAzyz zLzZLf=%2Jqy8wGHD;>^x57VG)sDZxU+EMfe0L{@1DtxrFOp)=zKY1i%HUf~Dro#8} zUw_Mj10K7iDsX}+fThqhb@&GI7PwONx!5z;`yLmB_92z0sBd#HiqTzDvAsTdx+%W{ z2YL#U=9r!@3pNXMp_nvximh+@HV3psUaVa-lOBekVuMf1RUd26~P*|MLouQrb}XM-bEw(UgQxMI6M&l3Nha z{MBcV=tl(b_4}oFdAo}WX$~$Mj-z70FowdoB{TN|h2BdYs?$imcj{IQpEf9q z)rzpttc0?iwopSmEoB&V!1aoZqEWEeO-MKMx(4iK7&Fhc(94c zdy}SOnSCOHX+A8q@i>gB@mQ~Anv|yiUsW!bO9hb&5JqTfDit9X6xDEz*mQEiNu$ay zwqkTV%WLat|Ar+xCOfYs0UQNM`sdsnn*zJr>5T=qOU4#Z(d90!IL76DaHIZeWKyE1 zqwN%9+~lPf2d7)vN2*Q?En?DEPcM+GQwvA<#;X3v=fqsxmjYtLJpc3)A8~*g(KqFx zZEnqqruFDnEagXUM>TC7ngwKMjc2Gx%#Ll#=N4qkOuK|;>4%=0Xl7k`E69@QJ-*Vq zk9p5!+Ek#bjuPa<@Xv7ku4uiWo|_wy)6tIr`aO!)h>m5zaMS-@{HGIXJ0UilA7*I} z?|NZ!Tp8@o-lnyde*H+@8IHME8VTQOGh96&XX3E+}OB zA>VLAGW+urF&J{H{9Gj3&u+Gyn?JAVW84_XBeGs1;mm?2SQm9^!3UE@(_FiMwgkJI zZ*caE={wMm`7>9R?z3Ewg!{PdFDrbzCmz=RF<@(yQJ_A6?PCd_MdUf5vv6G#9Mf)i#G z($OxDT~8RNZ>1R-vw|nN699a}MQN4gJE_9gA-0%>a?Q<9;f3ymgoi$OI!=aE6Elw z2I`l!qe-1J$T$X&x9Zz#;3!P$I);jdOgYY1nqny-k=4|Q4F!mkqACSN`blRji>z1` zc8M57`~1lgL+Ha%@V9_G($HFBXH%k;Swyr>EsQvg%6rNi){Tr&+NAMga2;@85531V z_h+h{jdB&-l+%aY{$oy2hQfx`d{&?#psJ78iXrhrO)McOFt-o80(W^LKM{Zw93O}m z;}G!51qE?hi=Gk2VRUL2kYOBRuAzktql%_KYF4>944&lJKfbr+uo@)hklCHkC=i)E zE*%WbWr@9zoNjumq|kT<9Hm*%&ahcQ)|TCjp@uymEU!&mqqgS;d|v)QlBsE0Jw|+^ zFi9xty2hOk?rlGYT3)Q7i4k65@$RJ-d<38o<`}3KsOR}t8sAShiVWevR8z^Si4>dS z)$&ILfZ9?H#H&lumngpj7`|rKQQ`|tmMmFR+y-9PP`;-425w+#PRKKnx7o-Rw8;}*Ctyw zKh~1oJ5+0hNZ79!1fb(t7IqD8*O1I_hM;o*V~vd_LKqu7c_thyLalEF8Y3oAV=ODv z$F_m(Z>ucO(@?+g_vZ`S9+=~Msu6W-V5I-V6h7->50nQ@+TELlpl{SIfYYNvS6T6D z`9cq=at#zEZUmTfTiM3*vUamr!OB~g$#?9$&QiwDMbSaEmciWf3O2E8?oE0ApScg38hb&iN%K+kvRt#d))-tr^ zD+%!d`i!OOE3in0Q_HzNXE!JcZ<0;cu6P_@;_TIyMZ@Wv!J z)HSXAYKE%-oBk`Ye@W3ShYu-bfCAZ}1|J16hFnLy z?Bmg2_kLhlZ*?`5R8(1%Y?{O?xT)IMv{-)VWa9#1pKH|oVRm4!lLmls=u}Lxs44@g^Zwa0Z_h>Rk<(_mHN47=Id4oba zQ-=qXGz^cNX(b*=NT0<^23+hpS&#OXzzVO@$Z2)D`@oS=#(s+eQ@+FSQcpXD@9npp zlxNC&q-PFU6|!;RiM`?o&Sj&)<4xG3#ozRyQxcW4=EE;E)wcZ&zUG*5elg;{9!j}I z9slay#_bb<)N!IKO16`n3^@w=Y%duKA-{8q``*!w9SW|SRbxcNl50{k&CsV@b`5Xg zWGZ1lX)zs_M65Yt&lO%mG0^IFxzE_CL_6$rDFc&#xX5EXEKbV8E2FOAt>Ka@e0aHQ zMBf>J$FLrCGL@$VgPKSbRkkqo>sOXmU!Yx+Dp7E3SRfT`v~!mjU3qj-*!!YjgI*^) z+*05x78FVnVwSGKr^A|FW*0B|HYgc{c;e3Ld}z4rMI7hVBKaiJRL_e$rxDW^8!nGLdJ<7ex9dFoyj|EkODflJ#Xl`j&bTO%=$v)c+gJsLK_%H3}A_} z6%rfG?a7+k7Bl(HW;wQ7BwY=YFMSR3J43?!;#~E&)-RV_L!|S%XEPYl&#`s!LcF>l zn&K8eemu&CJp2hOHJKaYU#hxEutr+O161ze&=j3w12)UKS%+LAwbjqR8sDoZHnD=m0(p62!zg zxt!Sj65S?6WPmm zL&U9c`6G}T`irf=NcOiZ!V)qhnvMNOPjVkyO2^CGJ+dKTnNAPa?!AxZEpO7yL_LkB zWpolpaDfSaO-&Uv=dj7`03^BT3_HJOAjn~X;wz-}03kNs@D^()_{*BD|0mII!J>5p z1h06PTyM#3BWzAz1FPewjtrQfvecWhkRR=^gKeFDe$rmaYAo!np6iuio3>$w?az$E zwGH|zy@OgvuXok}C)o1_&N6B3P7ZX&-yimXc1hAbXr!K&vclCL%hjVF$yHpK6i_Wa z*CMg1RAH1(EuuA01@lA$sMfe*s@9- z$jNWqM;a%d3?(>Hzp*MiOUM*?8eJ$=(0fYFis!YA;0m8s^Q=M0Hx4ai3eLn%CBm14 zOb8lfI!^UAu_RkuHmKA-8gx8Z;##oCpZV{{NlNSe<i;9!MfIN!&;JI-{|n{(A19|s z9oiGesENcLf@NN^9R0uIrgg(46r%kjR{0SbnjBqPq()wDJ@LC2{kUu_j$VR=l`#RdaRe zxx;b7bu+@IntWaV$si1_nrQpo*IWGLBhhMS13qH zTy4NpK<-3aVc;M)5v(8JeksSAGQJ%6(PXGnQ-g^GQPh|xCop?zVXlFz>42%rbP@jg z)n)% zM9anq5(R=uo4tq~W7wES$g|Ko z1iNIw@-{x@xKxSXAuTx@SEcw(%E49+JJCpT(y=d+n9PO0Gv1SmHkYbcxPgDHF}4iY zkXU4rkqkwVBz<{mcv~A0K|{zpX}aJcty9s(u-$je2&=1u(e#Q~UA{gA!f;0EAaDzdQ=}x7g(9gWrWYe~ zV98=VkHbI!5Rr;+SM;*#tOgYNlfr7;nLU~MD^jSdSpn@gYOa$TQPv+e8DyJ&>aInB zDk>JmjH=}<4H4N4z&QeFx>1VPY8GU&^1c&71T*@2#dINft%ibtY(bAm%<2YwPL?J0Mt{ z7l7BR718o5=v|jB!<7PDBafdL>?cCdVmKC;)MCOobo5edt%RTWiReAMaIU5X9h`@El0sR&Z z7Ed+FiyA+QAyWn zf7=%(8XpcS*C4^-L24TBUu%0;@s!Nzy{e95qjgkzElf0#ou`sYng<}wG1M|L? zKl6ITA1X9mt6o@S(#R3B{uwJI8O$&<3{+A?T~t>Kapx6#QJDol6%?i-{b1aRu?&9B z*W@$T*o&IQ&5Kc*4LK_)MK-f&Ys^OJ9FfE?0SDbAPd(RB)Oju#S(LK)?EVandS1qb#KR;OP|86J?;TqI%E8`vszd&-kS%&~;1Als=NaLzRNnj4q=+ zu5H#z)BDKHo1EJTC?Cd_oq0qEqNAF8PwU7fK!-WwVEp4~4g z3SEmE3-$ddli))xY9KN$lxEIfyLzup@utHn=Q{OCoz9?>u%L^JjClW$M8OB`txg4r6Q-6UlVx3tR%%Z!VMb6#|BKRL`I))#g zij8#9gk|p&Iwv+4s+=XRDW7VQrI(+9>DikEq!_6vIX8$>poDjSYIPcju%=qluSS&j zI-~+ztl1f71O-B+s7Hf>AZ#}DNSf`7C7*)%(Xzf|ps6Dr7IOGSR417xsU=Rxb z1pgk9vv${17h7mZ{)*R{mc%R=!i}8EFV9pl8V=nXCZruBff`$cqN3tpB&RK^$yH!A8RL zJ5KltH$&5%xC7pLZD}6wjD2-uq3&XL8CM$@V9jqalF{mvZ)c4Vn?xXbvkB(q%xbSdjoXJXanVN@I;8I`)XlBX@6BjuQKD28Jrg05} z^ImmK-Ux*QMn_A|1ionE#AurP8Vi?x)7jG?v#YyVe_9^up@6^t_Zy^T1yKW*t* z&Z0+0Eo(==98ig=^`he&G^K$I!F~1l~gq}%o5#pR6?T+ zLmZu&_ekx%^nys<^tC@)s$kD`^r8)1^tUazRkWEYPw0P)=%cqnyeFo3nW zyV$^0DXPKn5^QiOtOi4MIX^#3wBPJjenU#2OIAgCHPKXv$OY=e;yf7+_vI7KcjKq% z?RVzC24ekYp2lEhIE^J$l&wNX0<}1Poir8PjM`m#zwk-AL0w6WvltT}*JN8WFmtP_ z6#rK7$6S!nS!}PSFTG6AF7giGJw5%A%14ECde3x95(%>&W3zUF!8x5%*h-zk8b@Bz zh`7@ixoCVCZ&$$*YUJpur90Yg0X-P82>c~NMzDy7@Ed|6(#`;{)%t7#Yb>*DBiXC3 zUFq(UDFjrgOsc%0KJ_L;WQKF0q!MINpQzSsqwv?#Wg+-NO; z84#4nk$+3C{2f#}TrRhin=Erdfs77TqBSvmxm0P?01Tn@V(}gI_ltHRzQKPyvQ2=M zX#i1-a(>FPaESNx+wZ6J{^m_q3i})1n~JG80c<%-Ky!ZdTs8cn{qWY%x%X^27-Or_ z`KjiUE$OG9K4lWS16+?aak__C*)XA{ z6HmS*8#t_3dl}4;7ZZgn4|Tyy1lOEM1~6Qgl(|BgfQF{Mfjktch zB5kc~4NeehRYO%)3Z!FFHhUVVcV@uEX$eft5Qn&V3g;}hScW_d)K_h5i)vxjKCxcf zL>XlZ^*pQNuX*RJQn)b6;blT3<7@Ap)55)aK3n-H08GIx65W zO9B%gE%`!fyT`)hKjm-&=on)l&!i-QH+mXQ&lbXg0d|F{Ac#U;6b$pqQcpqWSgAPo zmr$gOoE*0r#7J=cu1$5YZE%uylM!i3L{;GW{ae9uy)+EaV>GqW6QJ)*B2)-W`|kLL z)EeeBtpgm;79U_1;Ni5!c^0RbG8yZ0W98JiG~TC8rjFRjGc6Zi8BtoC);q1@8h7UV zFa&LRzYsq%6d!o5-yrqyjXi>jg&c8bu}{Bz9F2D(B%nnuVAz74zmBGv)PAdFXS2(A z=Z?uupM2f-ar0!A)C6l2o8a|+uT*~huH)!h3i!&$ zr>76mt|lwexD(W_+5R{e@2SwR15lGxsnEy|gbS-s5?U}l*kcfQlfnQKo5=LZXizrL zM=0ty+$#f_qGGri-*t@LfGS?%7&LigUIU#JXvwEdJZvIgPCWFBTPT`@Re5z%%tRDO zkMlJCoqf2A=hkU7Ih=IxmPF~fEL90)u76nfFRQwe{m7b&Ww$pnk~$4Lx#s9|($Cvt ze|p{Xozhb^g1MNh-PqS_dLY|Fex4|rhM#lmzq&mhebD$5P>M$eqLoV|z=VQY{)7&sR#tW zl(S1i!!Rrg7kv+V@EL51PGpm511he%MbX2-Jl+DtyYA(0gZyZQjPZP@`SAH{n&25@ zd)emg(p2T3$A!Nmzo|%=z%AhLX)W4hsZNFhmd4<1l6?b3&Fg)G(Zh%J{Cf8Q;?_++ zgO7O<(-)H|Es@QqUgcXNJEfC-BCB~#dhi6ADVZtL!)Mx|u7>ukD052z!QZ5UC-+rd zYXWNRpCmdM{&?M9OMa;OiN{Y#0+F>lBQ=W@M;OXq;-7v3niC$pM8p!agNmq7F04;| z@s-_98JJB&s`Pr6o$KZ=8}qO*7m6SMp7kVmmh$jfnG{r@O(auI7Z^jj!x}NTLS9>k zdo}&Qc2m4Ws3)5qFw#<$h=g%+QUKiYog33bE)e4*H~6tfd42q+|FT5+vmr6Y$6HGC zV!!q>B`1Ho|6E|D<2tYE;4`8WRfm2#AVBBn%_W)mi(~x@g;uyQV3_)~!#A6kmFy0p zY~#!R1%h5E{5;rehP%-#kjMLt*{g((o@0-9*8lKVu+t~CtnOxuaMgo2ssI6@kX09{ zkn~q8Gx<6T)l}7tWYS#q0&~x|-3ho@l}qIr79qOJQcm&Kfr7H54=BQto0)vd1A_*V z)8b2{xa5O^u95~TS=HcJF5b9gMV%&M6uaj<>E zPNM~qGjJ~xbg%QTy#(hPtfc46^nN=Y_GmPYY_hTL{q`W3NedZyRL^kgU@Q$_KMAjEzz*eip`3u6AhPDcWXzR=Io5EtZRPme>#K9 z4lN&87i%YYjoCKN_z9YK+{fJu{yrriba#oGM|2l$ir017UH86Eoig3x+;bz32R*;n zt)Eyg#PhQbbGr^naCv0?H<=@+Poz)Xw*3Gn00qdSL|zGiyYKOA0CP%qk=rBAlt~hr zEvd3Z4nfW%g|c`_sfK$z8fWsXTQm@@eI-FpLGrW<^PIjYw)XC-xFk+M<6>MfG;WJr zuN}7b;p^`uc0j(73^=XJcw;|D4B(`)Flm|qEbB?>qBBv2V?`mWA?Q3yRdLkK7b}y& z+!3!JBI{+&`~;%Pj#n&&y+<;IQzw5SvqlbC+V=kLZLAHOQb zS{{8E&JXy1p|B&$K!T*GKtSV^{|Uk;`oE*F;?@q1dX|>|KWb@|Dy*lbGV0Gx;gpA$ z*N16`v*gQ?6Skw(f^|SL;;^ox6jf2AQ$Zl?gvEV&H|-ep*hIS@0TmGu1X1ZmEPY&f zKCrV{UgRAiNU*=+Uw%gjIQhTAC@67m)6(_D+N>)(^gK74F%M2NUpWpho}aq|Kxh$3 zz#DWOmQV4Lg&}`XTU41Z|P~5;wN2c?2L{a=)Xi~!m#*=22c~&AW zgG#yc!_p##fI&E{xQD9l#^x|9`wSyCMxXe<3^kDIkS0N>=oAz7b`@M>aT?e$IGZR; zS;I{gnr4cS^u$#>D(sjkh^T6_$s=*o%vNLC5+6J=HA$&0v6(Y1lm|RDn&v|^CTV{= zjVrg_S}WZ|k=zzp>DX08AtfT@LhW&}!rv^);ds7|mKc5^zge_Li>FTNFoA8dbk@K$ zuuzmDQRL1leikp%m}2_`A7*7=1p2!HBlj0KjPC|WT?5{_aa%}rQ+9MqcfXI0NtjvXz1U)|H>0{6^JpHspI4MfXjV%1Tc1O!tdvd{!IpO+@ z!nh()i-J3`AXow^MP!oVLVhVW&!CDaQxlD9b|Zsc%IzsZ@d~OfMvTFXoEQg9Nj|_L zI+^=(GK9!FGck+y8!KF!nzw8ZCX>?kQr=p@7EL_^;2Mlu1e7@ixfZQ#pqpyCJ```(m;la2NpJNoLQR};i4E;hd+|QBL@GdQy(Cc zTSgZ)4O~hXj86x<7&ho5ePzDrVD`XL7{7PjjNM1|6d5>*1hFPY!E(XDMA+AS;_%E~ z(dOs)vy29&I`5_yEw0x{8Adg%wvmoW&Q;x?5`HJFB@KtmS+o0ZFkE@f)v>YYh-z&m z#>ze?@JK4oE7kFRFD%MPC@x$^p{aW}*CH9Y_(oJ~St#(2)4e-b34D>VG6giMGFA83 zpZTHM2I*c8HE}5G;?Y7RXMA2k{Y?RxHb2 zZFQv?!*Kr_q;jt3`{?B5Wf}_a7`roT&m1BN9{;5Vqo6JPh*gnN(gj}#=A$-F(SRJj zUih_ce0f%K19VLXi5(VBGOFbc(YF zLvvOJl+W<}>_6_4O?LhD>MRGlrk;~J{S#Q;Q9F^;Cu@>EgZAH=-5fp02(VND(v#7n zK-`CfxEdonk!!65?3Ry(s$=|CvNV}u$5YpUf?9kZl8h@M!AMR7RG<9#=`_@qF@})d ztJDH>=F!5I+h!4#^DN6C$pd6^)_;0Bz7|#^edb9_qFg&eI}x{Roovml5^Yf5;=ehZ zGqz-x{I`J$ejkmGTFipKrUbv-+1S_Yga=)I2ZsO16_ye@!%&Op^6;#*Bm;=I^#F;? z27Sz-pXm4x-ykSW*3`)y4$89wy6dNOP$(@VYuPfb97XPDTY2FE{Z+{6=}LLA23mAc zskjZJ05>b)I7^SfVc)LnKW(&*(kP*jBnj>jtph`ZD@&30362cnQpZW8juUWcDnghc zy|tN1T6m?R7E8iyrL%)53`ymXX~_;#r${G`4Q(&7=m7b#jN%wdLlS0lb~r9RMdSuU zJ{~>>zGA5N`^QmrzaqDJ(=9y*?@HZyE!yLFONJO!8q5Up#2v>fR6CkquE$PEcvw5q zC8FZX!15JgSn{Gqft&>A9r0e#be^C<%)psE*nyW^e>tsc8s4Q}OIm})rOhuc{3o)g1r>Q^w5mas) zDlZQyjQefhl0PmH%cK05*&v{-M1QCiK=rAP%c#pdCq_StgDW}mmw$S&K6ASE=`u4+ z5wcmtrP27nAlQCc4qazffZoFV7*l2=Va}SVJD6CgRY^=5Ul=VYLGqR7H^LHA;H^1g}ekn=4K8SPRCT+pel*@jUXnLz+AIePjz@mUsslCN2 z({jl?BWf&DS+FlE5Xwp%5zXC7{!C=k9oQLP5B;sLQxd`pg+B@qPRqZ6FU(k~QkQu{ zF~5P=kLhs+D}8qqa|CQo2=cv$wkqAzBRmz_HL9(HRBj&73T@+B{(zZahlkkJ>EQmQ zenp59dy+L;sSWYde!z_W+I~-+2Xnm;c;wI_wH=RTgxpMlCW@;Us*0}L74J#E z8XbDWJGpBscw?W$&ZxZNxUq(*DKDwNzW7_}AIw$HF6Ix|;AJ3t6lN=v(c9=?n9;Y0 zK9A0uW4Ib9|Mp-itnzS#5in=Ny+XhGO8#(1_H4%Z6yEBciBiHfn*h;^r9gWb^$UB4 zJtN8^++GfT`1!WfQt#3sXGi-p<~gIVdMM<#ZZ0e_kdPG%Q5s20NNt3Jj^t$(?5cJ$ zGZ#FT(Lt>-0fP4b5V3az4_byF12k%}Spc$WsRydi&H|9H5u1RbfPC#lq=z#a9W(r1 z!*}KST!Yhsem0tO#r!z`znSL-=NnP~f(pw-sE+Z$e7i7t9nBP^5ts1~WFmW+j+<@7 zIh@^zKO{1%Lpx^$w8-S+T_59v;%N;EZtJzcfN%&@(Ux5 z@YzX^MwbbXESD*d(&qT7-eOHD6iaH-^N>p2sVdq&(`C$;?#mgBANIc5$r| z^A$r)@c{Z}N%sbfo?T`tTHz9-YpiMW?6>kr&W9t$Cuk{q^g1<$I~L zo++o2!!$;|U93cI#p4hyc!_Mv2QKXxv419}Ej#w#%N+YIBDdnn8;35!f2QZkUG?8O zpP47Wf9rnoI^^!9!dy~XsZ&!DU4bVTAi3Fc<9$_krGR&3TI=Az9uMgYU5dd~ksx+} zP+bs9y+NgEL>c@l>H1R%@>5SWg2k&@QZL(qNUI4XwDl6(=!Q^U%o984{|0e|mR$p+ z9BcwttR#7?As?@Q{+j?K6H7R71PuiA^Dl$=f47nUKL|koCwutc_P<-m{|Al3C~o7w z=4S=}s5LcJFT1zjS)+10X_r$74`K78pz!nGGH%JV%w75!YSIt#hT7}}K>+@{{a+Im z5p#6%^X*txY?}|T17xWW*sa^?G2QHt#@tlcw0GIcy;|NR2vaCBDvn=`h)1il7E5Rx z%)mA4$`$OZx)NF5vXZnaJ1)*cA6ryx6Ll~t!LzhxvcTedxT;>JS&e=?-&DXUPaQ2~ zH*69ezE`hgV{K-|0z|m~ld}=X^-Ob={wpex&}*+Rz{gx)G}gn!C_VN{UN=>^EV=Xc zr$-HO09cW&p4^M}V3yBjTP_xrVcc8iU_^Y-JD~(bgw*@GXGB1gYKz5DWO+O`>})|N zWrC)MR93yA)3{&27-M)TJB6Ml3~?zZg#mYsF=#OSTaw&K z@hBftpt+2l@)YK@|3DvTjl(8wZtpLp9Ik!6G$CSL_idZ$Ti?R)4toe8bb)l|)lNb}?K;O2K9vyn1QG zd=v#y-Ld49UVkmfRU>Egc+(Y$^-;6vW;3Lcu*6~etz}0|@+b|+!UCal)DEYGLbHWJ zll5Wi^$Y<6@S%^y%hdjRh6&{!z1Py|lZ|q&Wub3l41uN2zEF8E&5H5?PL*&V}?*a}Lp% zCYi{ghjpRNT^^B+_U59No50Ghih5qn(W5`RkrsDWr{~A1dgtv{sRkH4RU2^A{jb&0 zxVRnrm|u<;$iI;M6A>$POP)TWGU-gSjAERk*EGmVT(aw$!XUSe~7Ql-oRA54^4V(JWS6Q1mG?!vZ zx+pE!FEtvqr|Xrcb3oR`%LHFLmU_&{=p%mGy6MRe2Yz_5WJ8p@IgU2 zdVvvhhQtiQkChK%*&PsiPCBL9oDOoJX8!$S(V>R}+1M}wzK*U*A{KJ`r=lM;mPrKU zQDqqN(W*u-5-?$(SIk<6A0E}34y&@-IVC%S!a1F4kz<3bIKjlyD)ooO_7ftl%S_(6w`!vX&1PZ!K`@D@L6JR)6zO@Dl!YF{RY}d3HZ7?Q5E>w=$ ze)H_)48Ds*Ov4?zoGb2fe3}{!5Ooc|KCIni1o)(Gj+CO?`*7jsV`hIv@8J(22o4Q? zu?Bvi)zDG(me?7XKeL|iF9ZRgZdT*}Ffsl62Cu;{Gv9j6dO zPt*H2GqC)-C`V`ceuu=tM{7!2yTEj=*5+T~5DYiZ)Hy)*PARYI6R2lZXoOj;v8M4W z*O-NX(7_~Q&A3>Oaw&1lBH_H%SwmISX-i3)HfHvBOeVwTT{LUM3}ZuZmg<(>)KE;d zbs2!0v6>J;1nQ0UJkUxnkE@Ibi~Q}M=-=Rk;hcOnxO$luOKEVxZc|!XECgex(2`}T z3Y;Q_6rL)e+SrOZhQj5_e}Lv>w7n*Pep$yWZNQl>ubBgb_NIWWDn3kNpn+MPQXV;8 zV|_Ba5jsQ(w&Ey^IM|@|y!AqcJ#3m0#Q6_qvgCG~eoF#mnGmbO(;DP+bW%_aOs1R_ z@9p#7X2UA^--#Nwx_Hvk2l1`eO{P*#j@q2UELtH|Uh6hxR`h_847wIJo0=5CQQ`6it|%a-I$^&a@we1rc&*;QIu5Ck^?) zx*5eSd*mG#=6Hi(5!;5uUi&{HfnT1S8X-)?gE5CZ6KWoqM5|CyrULmuFBKOU8SOp* z{IB1$OCcq`S-k*xs;4fmhKsIGZ;GYAY*%(@875NxhMq|j*m4CNLI(Vho|N|F);!E0cS5y^$H^Izje?z}oTgyr`9x9G&rlJZw&uqIoBMtz zzhU0(9;w02?m#0!)cFi*r+8YvooQ;(s2lLVvyLqAE%Xqe!vtWbIs!l1Bpp(FIht-Z zPn#CN-2C|J*GhA2fuHqYQ2mJiXlGTzD}mkr2;ia8Wp}h^;OS7+N^Mw|en!1${vN6 z-x{8N*4UekA~`IV2&K-GzhAqau|}d*pEQ$1MH$cFi03OG^1NetZ_jW^STaEzr&Xho zB452St%v3ez2#TFm~`gZh$vi=in+y2d!z<{OZ~Kty-5bQ;0O=k_ESi8Nx9{*T`LJy6jqR>&|+>OZ;+=0hA04 zE25t^sE9HG)3^KKR_A5WDkqispweP9!I-@dCO&N!JrD@i{WBHnfQ z95o8;d$`AFnca3;N-0iX-CmbbAp5yQ!GoH;h7Cn?m{ammZJI8igP{U73lFnl2&gCs zqJ4(Vo~^j`{zOAzScL5B_Sm?Mjtek1d(A6X5ObcZi$;aOYy|g$}BY z$GEP3#i60Ju_&3SHzryH!gUFwC9-295u??cf+aYRQ1$+!rc#42YNattd6mZEFI@?C zqFM>6+zxEunIHDZ>{Z15u##>N(28Dw!>G(k*dB{NHvip@aP}f`@=Q;!o;zRMWo{Cx zo?kyzh8n7#f1g0&g>Cd>O-2g?uPwy8sy8hZbHSsXPmU;@l=HL=zm7mN(=@*|D$i+u zs~TllkCTvD$f&-#b9B?}#Lg*-ibK13R_a$RyoN3m5`10tdhAq{+VW)K#Bht-ra1*J z+n$N%V>u0rVtx`aKJDwXXrxaD7nS<>$=c82v7@KVx^S@vT;h=SZE37K>iahpx3;VDzEr9GY=2(%uaqM;^76eSP0QLzo4sI z>p_Eei*T$K;|qK`sq;?Hesp}(@VvX2Q4sAMYAJ}b&d$htDMC{FG-$o4k9ApECi1$a zXdamjiOGKHBh(4M<3(2x6n-CrmZMCknkQxdSS!qlis#I}btfX;J`JU3RlvtLdrymP zG0ZzrsGXVFiq+Wk1=BFay&9ZiCE#(`h~CL+c-Hs@iGTU@YxM%vlg;)`Tf~IknA^02 zXkN#Txo6aR{j$wP5T#|UH#5AP2{rSY8p?jKFv zG3kn3y`FaV!*Jq%m39_TQEhD>M@l*bhEPGe1{ft3q#K5AknT=F2_=T^l#ou5ln@D# z5Tzs(kRG@qNDa~HLNvfv7Z0g=bSlb?`QAx|Gfoni|iHJ%K0cy z;~Nsaa+{8HP_qrb{nj+xzkdYhSI@W4N_1`z(eSGIkbDP)!Ko|M%}Rqp(~KI2hl~eE zvJ!j4m6iwMgKy>fkCLC)`M$z9EV}B+sq1}}kVf$(ig0pWTY?rHz1Sm=4srTGNb^JG z=2$9wz-C@aZZZ2!HY#HNejqZRmE=pN(D$Kui$NpfhU`!y_s{@MIxiJdHb1|{6xb`> zE74_@QtgtG{4=3P1$^vn&m}7Aw8!1DnT$2thO#~44wl(N#ao8S0@t@m+Z!KD2CfK; z)n5DAPKV_etmH1aLDK$?`;sL91iVt$D z*SG}=-LIAg(*+JON!-5ivqOMQ1S!OQUgHglDsKik&Mwg;vva523`JwQH6SRz9eTY# zTIi23145~kc3r1mSWC_RzD%hs$S#!pkI9!BU80jJCJcwo*FZolQG$q`8C1d9pP@ND zG^&-ZraIvhg_FDVSfKGwkcI=avIan%2sK4coUs~Nr8jC*&!G0#?}_^s3r-c}-uAqi zM-Lw>Y}I``T;IS%Y|qH;s{F*ZefM!4{I5awr!K+T@uPd*Vu*iPWI}>(-D{zxsN>LG z=@747a_Rb2>q?y8xYf?dq2HM5tFO8Y5e4N;Y=xy8yAhI zsm>oy%R5;7)7T3V_b2%`aH^tNlsQpFxIFW#iV#8?{6{^cGr{A0@1bA)|K z>MMTuZD(pd2t|7vmHtywGXb%%=)S<`OG~}U+jm#xd%H8 z$v8-C%F?ah3$;hn?{G3(LT!SgvCVi$vwsZssAQvUwT`Q%qSw!LSd!(I!64w1=%Sc1Mck)q1@pZ@)=SY zoX}d+L3-RA|c?G3_BQNm&( z!i$AZ7cI(z7q|e9VM##6T3Xorj1JG(9os$;(I$y%mBy(#8{|3l4|x*oBAQL^XhZ0g zy1FR1teRrpKq{uLAibTLx#n({qwjlkOvR{OdSAeT5ah4-sNN)n4Clg1T9lzF)&yj; zyal1%+s4n1IG;^VPWJ;#olpk8Z42Gj-tjFeQ&PlxB)`oCNoUYKj4U$AeG8rYiD{pK zndDf&2;2;)D|KvOZP+e7fcPU9k4M2sfhr@vC~Ly0?S-4dz)ZGAYpCsAhChgbxLd4g zhTrbIPkO5SEp_kD>Ha0m12h5n3s;mE8kn515&nzSf+^D= zyE{JnJ;43l&BH55CL<=W%CF;6iUI)V5C*6!`**KqvzR2=Fj*3Y4`HYwx}TYD445(K z-QtXwtL?m*(F=LVH*H4oM>dXHBW=38q_dZ-_Vr&qpEPxd9Fs95P5W~@Z|Rt+WZP6l zPSQ}~Dh4V?Pp1g&Hk*Px?lm16C@X6M29Vrk%Rw@E||E-v~$ zb_E~{z<}#8i`Mx9mkqtd#Z1lZ-E_J8I+2oumc#x1)jdvh{W76NKm6x-RYpM~v!P8$ zw3e|YVf|}Hse9~oC@N7^j}Fi$hNpyaYnu1}bdXsD=^oI*%WKvbme|BI}$G3>smu#6y)ls|j? zF7Bhu9Z)j)C;3cZb+I>0stSK^WLOYV^U{pUYkgv>?+Nt^5j*CUB=eGw-CvU&40>y~ zGoHLXxY^7k5Xgv62{iQy|5jJQuq0|LU`}lE@flQ2Z*Zn*VWcQjm4FTb>LSVox^S4q zLn`LfS@mrjKCmg$nb^af?d?0&$aX6#2u(JyzIJvuJ*lwPrh|0~aEnSACCTezSdG%h zmSQg`17j@$Iq)r1&?+eR@1nlX|H`<}_!?BQSF&N+QQnvEAqZe+mIFui!0V49R?|9*$ zv!K1A01{8xq;L()Tv*Qk0-$Oj6+vCT*TUD{HvxO@3JjxBwM!4g3ydy&eaJw4CoQBF zJtULJ!YxgNR7_Ls%LmogyI7uIs=!B&?=MYY^yX+v;j@D_xGeZg>eZk0C;4e|HRNSi z6KlD9>q=3v-$4Zik&^ZDhNm1X)+7LCH1k!s+T3tn zUn@={1U&NJLq@K?~w|(=Y<4W{ucX}FdRr6pLw(l2$iK)At%t3gYBMlJz#(K0Nqm;=KAML!&MMSNz=%k=j*zh77r34Rs37iCY` z=_kva_41bdrj(b=4Wc5MO0~q^z#pIWJ>)vDSgIQF=3JVJe1iDy%h)8oNy{s_r&;m` zL{DYKSB_5xRb9xKNOS{qAY3qv5sSXVrrf%~*q5HO|CQ&lbKMePa$M5D{vlJcoGrCZ zD?fKbZN$6rWwz)w7`9h4DAmh1ij2}EO|bO#A9L0_RW6l*$sPPUJrUbhLC75L9%W5iO$Iw5~Yut-qBeu~hF|xD7-eQ%l z412vpq_;t%^F*pYDk%Q35c-erK|6Ve=FxQbAv~ikZ4c9$Y4;ee#ciOD9{yRqf55Qk zumv}#+JciT|Gj$uFOxBUze)=?l{B}qaC0_7m`t82<$K53!4Xvi9Tr)ADp3Off?O8o zVDG0Yx|tfn@r((m?Nxrh(b0DGjg)$;DfO&$6uY;4&F!4jnxkhP}Y3x zS?WFFt>=HWzqlQhffVfvM$Ta8Sg*r3j!Eo&rUOW7SCL2~lG7<+XZ;+{&8h5g8ElI+P>>yR2U%S93NN!Xhm|C682t6ysH-=o1=Bd*N*VlnG%l+KZFtjG`UkL;%65qn0UYQ`h zh0{9jDQx(`aBe7J0Aj3Z)4}`A|4OMM0a;?{j}qkYwi)~O8$9D}ITiMH2buiU>ixYp zhL${nwj6X($*OwmpVG`y5b6v45tX*J8?og}Qju6eJ9H}`X87iEd%BUo7<`2q(HJx+ zMR}d-J4oAf{V1W^a2~`M-YAdZ81dd4o6NPO{cmZaAS@RS4ir#Sr zfFZO-VIL|VN<%nEXr2` z$0FK2L#8O_f1w~c@G70JrB@N}r(gJ!Vmkk6{r68w!o$qO?HrFcjeU0_3F5;*!E2%( zTx>4?gP8w z1B?3UVZmz^%d_dIps>>0{cB~mp3{9UoPR6uQFecVq&} zY{ebB?AlPAD_}(ll{fK99;Wh1cgRbnw)maD^F>*J!R}eHM*W0VYN1TADWMy9H=$00 z5bHY${oDgwX7(W9LZw?}{!8(_{JB~Xkje6{0x4fgC4kUmpfJ+LT1DYD*TWu4#h{Y7 zFLronmc=hS=W=j1ar3r1JNjQoWo2hMWsqW*e?TF%#&{GpsaLp}iN~$)ar+7Ti}E&X z-nq~+Gkp(`qF0F_4A22>VZn-x>I$?PDZSeG8h_ifoWf^DxIb5%T7UytYo3}F|4#RC zUHpg$=)qVqD~=m(!~?XwocuxU1u}9qhhM7d^eqmJPi_e-!IO`*{u7A zbu*?L$Mbj-X9n3G2>+Kc#l`@d8}Xb9{l*IN{#M*d;s+3Pdr8FO$EBELR=8{ zd?LJbSv9fI`{OqTH)5{b?WulgMb)psp+W|@cSp=jtl-&5C}9lw@*0H+gEW(}mAWNz zf{~U;;N}|wdSaphgqnH{FWUy!{y3^=AC*c?RJ5Eb<^ zCgH_v7^axIUVmHSFL^zlj2R$zow$|y#7>%#U7d#Vp_ezcp3lefMyd5ES=q$>4pWyA zp_Zso^^NP~lu2=S6nD(3Z5u=Uy&B&F1i$J*3;3KhEkD_lgscHGR*;T;U!9vgQa(hI}oh9IzEf_PU_8F+i77t-~gDX z490Sb)LyVZmf18N6w{+37$aO<2!Av0 ztLaPOv^J<2@p{WnMiDudoghX_`luFZt_4eNU}*~cF5i%eEcNLs;D>QVIwr8mH;=dc z09`}JV;aaF;13@&iS(w>Jc=k~|d_1hcpM(l|O zu>!@}me%isTT$xT#hNUvh(ATd0wT4fbv=6htcHNEZIw9%E6wlYmwfu2{j0kh1y=$;Yf!|NldgB9ul zB{dbE&LfRnr8ITm@;-68wo#VV?8lG3ed&9k1}QBS3}WGV9%26?A1rBkkDR9Z3o+g+ z)eQg8BY3y(Dh5&z?VLLNdDV`C=muUvCPpGg!oYxIgOI3^%4>5d7jTh~ni!Fg2;fhx z(*c%H6Je84kmQh;5tC3*l~7khLxK-e|Cz?FLh!yYe7g|*LwqU?2wv^_ZyKT$fYVkGJo@AK0$+ml?}zJeB~deT2WL1vz}dxB z)y??t!}%M@)u$_IyW~)6u1SttJ!awd6N5lx|xBrmyrBh>tb&D*=C+Z3nPfq$1%WgY0bY*?PZ#Hk|=xn zGM#0*w4CaB^y0G(J4q=;5NeM@m-P}#mv7QZNF)M!dK^w{mk_!n0`+Y3PQutu-%NBt zzgPXug?JLEbUL{e_dk;Vd896&yPe(hliVK!lj%5+@BKdcrEZ2Nc_*i@ve*2lB>u~{ zFozd2FM|_0+nAGR4TLNHanQn_Oeb!JrUcvzJ?7p9TTNB}ocO3j$7ij!li8#k6 z@2tSd1>K03K9A#_-MIq)S;T#oE^;>U$)&}okIvDf3lm?kI{d80$>~xKUoS!%q1Pi?WpsUUt(tI ztjNjY*y&Rm9(S(DC2GuPHBJs@5M{RGm`c1z<6nwyN^)rMo-AS{M2$oM9|y%fM|}G~ DHx0+F literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0f5036d --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'backend' diff --git a/src/main/java/com/webeye/backend/BackendApplication.java b/src/main/java/com/webeye/backend/BackendApplication.java new file mode 100644 index 0000000..6366ab2 --- /dev/null +++ b/src/main/java/com/webeye/backend/BackendApplication.java @@ -0,0 +1,13 @@ +package com.webeye.backend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BackendApplication { + + public static void main(String[] args) { + SpringApplication.run(BackendApplication.class, args); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..3ca17a4 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1 @@ +spring.application.name=backend diff --git a/src/test/java/com/webeye/backend/BackendApplicationTests.java b/src/test/java/com/webeye/backend/BackendApplicationTests.java new file mode 100644 index 0000000..2cf5d31 --- /dev/null +++ b/src/test/java/com/webeye/backend/BackendApplicationTests.java @@ -0,0 +1,13 @@ +package com.webeye.backend; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BackendApplicationTests { + + @Test + void contextLoads() { + } + +} From b112c56719aba477ec4030e4d2ebf19a05f1b1b7 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 5 Apr 2025 14:38:55 +0900 Subject: [PATCH 005/309] =?UTF-8?q?#1=20chore:=20swagger=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 3a857e0..6eca3fc 100644 --- a/build.gradle +++ b/build.gradle @@ -24,16 +24,32 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + //swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' } tasks.named('test') { useJUnitPlatform() } + +def generated = 'src/main/generated' + +tasks.withType(JavaCompile).configureEach { + options.getGeneratedSourceOutputDirectory().set(file(generated)) +} + +sourceSets { + main.java.srcDirs += [ generated ] +} + +clean { + delete file(generated) +} From 37ff0d965c75ecad14048ba5af414214d69e47d1 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 5 Apr 2025 14:39:15 +0900 Subject: [PATCH 006/309] #1 feat: swagger config --- .../backend/global/config/SwaggerConfig.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/com/webeye/backend/global/config/SwaggerConfig.java diff --git a/src/main/java/com/webeye/backend/global/config/SwaggerConfig.java b/src/main/java/com/webeye/backend/global/config/SwaggerConfig.java new file mode 100644 index 0000000..d476e73 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/config/SwaggerConfig.java @@ -0,0 +1,16 @@ +package com.webeye.backend.global.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.servers.Server; + +@OpenAPIDefinition( + info = @Info(title = "WebEye API 명세서", + description = "WebEye API 명세서", + version = "v1" + ), + servers = @Server(url = "/api", description = "Default Server URL") +) + +public class SwaggerConfig { +} From 2732ea6ad53a06eed390ed8fdb581467eeb480fb Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 5 Apr 2025 14:40:05 +0900 Subject: [PATCH 007/309] =?UTF-8?q?#1=20feat:=20Success=20Reesponse=20?= =?UTF-8?q?=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/dto/response/SuccessResponse.java | 20 +++++++++++++++++++ .../global/dto/response/type/SuccessCode.java | 16 +++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/main/java/com/webeye/backend/global/dto/response/SuccessResponse.java create mode 100644 src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java diff --git a/src/main/java/com/webeye/backend/global/dto/response/SuccessResponse.java b/src/main/java/com/webeye/backend/global/dto/response/SuccessResponse.java new file mode 100644 index 0000000..a6c5abf --- /dev/null +++ b/src/main/java/com/webeye/backend/global/dto/response/SuccessResponse.java @@ -0,0 +1,20 @@ +package com.webeye.backend.global.dto.response; + +import com.webeye.backend.global.dto.response.type.SuccessCode; + +public record SuccessResponse( + int status, + String message, + T data +) { + + private static final String NOTHING = ""; + + public static SuccessResponse of(SuccessCode code) { + return new SuccessResponse<>(code.getStatus(), code.getMessage(), NOTHING); + } + + public static SuccessResponse of(SuccessCode code, T data) { + return new SuccessResponse<>(code.getStatus(), code.getMessage(), data); + } +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java new file mode 100644 index 0000000..d19d0ac --- /dev/null +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -0,0 +1,16 @@ +package com.webeye.backend.global.dto.response.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum SuccessCode { + + // health + HEALTH_CHECK_SUCCESS(200, "Health Check Success"), + ; + + private final int status; + private final String message; +} From 2a8747d2f057ff2e2955e3430091a2f2f5d1423a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 5 Apr 2025 14:40:26 +0900 Subject: [PATCH 008/309] =?UTF-8?q?#1=20feat:=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/error/BusinessException.java | 13 ++++++++ .../backend/global/error/ErrorCode.java | 13 ++++++++ .../backend/global/error/ErrorResponse.java | 18 +++++++++++ .../global/error/GlobalExceptionHandler.java | 32 +++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 src/main/java/com/webeye/backend/global/error/BusinessException.java create mode 100644 src/main/java/com/webeye/backend/global/error/ErrorCode.java create mode 100644 src/main/java/com/webeye/backend/global/error/ErrorResponse.java create mode 100644 src/main/java/com/webeye/backend/global/error/GlobalExceptionHandler.java diff --git a/src/main/java/com/webeye/backend/global/error/BusinessException.java b/src/main/java/com/webeye/backend/global/error/BusinessException.java new file mode 100644 index 0000000..4ac3fb5 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/error/BusinessException.java @@ -0,0 +1,13 @@ +package com.webeye.backend.global.error; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getErrorMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/webeye/backend/global/error/ErrorCode.java b/src/main/java/com/webeye/backend/global/error/ErrorCode.java new file mode 100644 index 0000000..2630ae6 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/error/ErrorCode.java @@ -0,0 +1,13 @@ +package com.webeye.backend.global.error; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + ; + private final HttpStatus status; + private final String errorMessage; +} diff --git a/src/main/java/com/webeye/backend/global/error/ErrorResponse.java b/src/main/java/com/webeye/backend/global/error/ErrorResponse.java new file mode 100644 index 0000000..3848f55 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/error/ErrorResponse.java @@ -0,0 +1,18 @@ +package com.webeye.backend.global.error; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ErrorResponse { + private final int status; + private final String errorMessage; + + public static ErrorResponse create(final ErrorCode errorCode) { + return new ErrorResponse( + errorCode.getStatus().value(), + errorCode.getErrorMessage() + ); + } +} diff --git a/src/main/java/com/webeye/backend/global/error/GlobalExceptionHandler.java b/src/main/java/com/webeye/backend/global/error/GlobalExceptionHandler.java new file mode 100644 index 0000000..d8dd434 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/error/GlobalExceptionHandler.java @@ -0,0 +1,32 @@ +package com.webeye.backend.global.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + protected ResponseEntity handleRuntimeException(BusinessException e) { + final ErrorCode errorCode = e.getErrorCode(); + log.warn("🚨Error Log: {}",e.getMessage()); + + return ResponseEntity + .status(errorCode.getStatus()) + .body(new ErrorResponse(errorCode.getStatus().value(), + errorCode.getErrorMessage())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException(MethodArgumentNotValidException e) { + log.warn(e.getMessage()); + return ResponseEntity + .status(e.getStatusCode()) + .body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage())); + } +} From afa7e47bada430fd0317ebce131d5ef280d3a220 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 5 Apr 2025 14:40:51 +0900 Subject: [PATCH 009/309] #1 feat: application.yml --- src/main/resources/application.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3ca17a4..e69de29 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1 +0,0 @@ -spring.application.name=backend From 2435acadb4201931ac6fc3a01bb5b3b7a8dd5765 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 5 Apr 2025 14:41:10 +0900 Subject: [PATCH 010/309] #1 feat: health check api --- .../global/presentation/GlobalController.java | 22 ++++++++++++++++++ .../presentation/swagger/GlobalSwagger.java | 23 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/main/java/com/webeye/backend/global/presentation/GlobalController.java create mode 100644 src/main/java/com/webeye/backend/global/presentation/swagger/GlobalSwagger.java diff --git a/src/main/java/com/webeye/backend/global/presentation/GlobalController.java b/src/main/java/com/webeye/backend/global/presentation/GlobalController.java new file mode 100644 index 0000000..3cb12b4 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/presentation/GlobalController.java @@ -0,0 +1,22 @@ +package com.webeye.backend.global.presentation; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.global.presentation.swagger.GlobalSwagger; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.HEALTH_CHECK_SUCCESS; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/global") +public class GlobalController implements GlobalSwagger { + @GetMapping("/health-check") + @Override + public ResponseEntity> healthcheck() { + return ResponseEntity.ok(SuccessResponse.of(HEALTH_CHECK_SUCCESS)); + } +} diff --git a/src/main/java/com/webeye/backend/global/presentation/swagger/GlobalSwagger.java b/src/main/java/com/webeye/backend/global/presentation/swagger/GlobalSwagger.java new file mode 100644 index 0000000..05ca3ab --- /dev/null +++ b/src/main/java/com/webeye/backend/global/presentation/swagger/GlobalSwagger.java @@ -0,0 +1,23 @@ +package com.webeye.backend.global.presentation.swagger; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +@Tag(name = "Global", description = "설정 확인을 위한 API") +public interface GlobalSwagger { + @Operation( + summary = "health check", + description = "health check API" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "health check success" + ) + }) + ResponseEntity> healthcheck(); +} \ No newline at end of file From a35f2c6d5b667e0b6f5f80a4355da3ec87569a3c Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 15:18:12 +0900 Subject: [PATCH 011/309] #3 docs: feature template --- .github/ISSUE_TEMPLATE/feature_template.md | 16 ++++++++++++++++ .github/ISSUE_TEMPLATE/issue_template.md | 18 ------------------ 2 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/feature_template.md delete mode 100644 .github/ISSUE_TEMPLATE/issue_template.md diff --git a/.github/ISSUE_TEMPLATE/feature_template.md b/.github/ISSUE_TEMPLATE/feature_template.md new file mode 100644 index 0000000..70ee089 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_template.md @@ -0,0 +1,16 @@ +--- +name: feature_template +about: 해당 이슈 템플릿을 사용하여 이슈를 생성해주세요. +title: 'feat: ' +labels: ['✨feature'] +assignees: '' + +--- + +## 📌 어떤 기능인가요? + + + +## 📝 작업 상세 내용 + +- [ ] TODO \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md deleted file mode 100644 index c9af9b8..0000000 --- a/.github/ISSUE_TEMPLATE/issue_template.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: issue_template -about: 해당 이슈 템플릿을 사용하여 이슈를 생성해주세요. -title: '' -labels: '' -assignees: '' - ---- - -## 어떤 기능인가요? - -> 추가하려는 기능에 대해 간결하게 설명해주세요 - -## 작업 상세 내용 - -- [ ] TODO -- [ ] TODO -- [ ] TODO From 0a260cc5d45e3c0140d681f415ecd3b88fb8f9e7 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 15:19:46 +0900 Subject: [PATCH 012/309] #3 docs: bug template --- .github/ISSUE_TEMPLATE/bug_template.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_template.md diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md new file mode 100644 index 0000000..5fdc9ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_template.md @@ -0,0 +1,19 @@ +--- +name: Bug Report Template +about: '버그 리포트 이슈 템플릿 ' +title: 'fix: ' +labels: ['🐛bug'] +assignees: '' + +--- + +## 🐛 어떤 버그인가요? + + + +## 🤷‍♂️ 어떤 상황에서 발생한 버그인가요? + + + +## 🤔 예상 결과 + \ No newline at end of file From c95a0ee4cd6d3e08f5c677bb77fbe72b6c12353a Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 15:20:28 +0900 Subject: [PATCH 013/309] =?UTF-8?q?#3=20docs:=20feature=20template=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/feature_template.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_template.md b/.github/ISSUE_TEMPLATE/feature_template.md index 70ee089..841adf1 100644 --- a/.github/ISSUE_TEMPLATE/feature_template.md +++ b/.github/ISSUE_TEMPLATE/feature_template.md @@ -8,9 +8,7 @@ assignees: '' --- ## 📌 어떤 기능인가요? - ## 📝 작업 상세 내용 - - [ ] TODO \ No newline at end of file From d3df85e1bd8c9691e718f380780e7053bed0e392 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 15:21:25 +0900 Subject: [PATCH 014/309] #3 docs: refactor template --- .github/ISSUE_TEMPLATE/refactor_template.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/refactor_template.md diff --git a/.github/ISSUE_TEMPLATE/refactor_template.md b/.github/ISSUE_TEMPLATE/refactor_template.md new file mode 100644 index 0000000..380e4ae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor_template.md @@ -0,0 +1,15 @@ +--- +name: Refactor Template +about: '리팩토링 템플릿' +title: 'refactor: ' +labels: ['♻️refactor'] +assignees: '' + +--- + +## 📌 어떤 기능인가요? + + + +## 📝 작업 상세 내용 +- [ ] TODO \ No newline at end of file From a1f6fecbe3fcb552d5026e89e2116adb47eb4468 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 15:22:35 +0900 Subject: [PATCH 015/309] #3 docs: PR template --- .github/pull_request_template.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 75ff8d0..fcd457d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,12 +1,15 @@ -## #️⃣연관된 이슈 -> ex) #이슈번호, #이슈번호 +## #️⃣ 연관된 이슈 -## 📝작업 내용 -> 이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능) +- -### 스크린샷 (선택) +## 📝 작업 내용 + -## 💬리뷰 요구사항(선택) -> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요 -> -> ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요? +- + +## 📸 스크린샷 + +- + +## 💬 리뷰 요구사항 + From 4d836da21c3e76adc86a7317b8b2cd9949caeff0 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 15:23:20 +0900 Subject: [PATCH 016/309] =?UTF-8?q?#3=20docs:=20feature=20template=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/feature_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_template.md b/.github/ISSUE_TEMPLATE/feature_template.md index 841adf1..9193368 100644 --- a/.github/ISSUE_TEMPLATE/feature_template.md +++ b/.github/ISSUE_TEMPLATE/feature_template.md @@ -1,6 +1,6 @@ --- name: feature_template -about: 해당 이슈 템플릿을 사용하여 이슈를 생성해주세요. +about: '기능 추가 템플릿' title: 'feat: ' labels: ['✨feature'] assignees: '' From 5ef52e1e79b8903ef4f3084bb5c3be26c47dbff0 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 16:41:55 +0900 Subject: [PATCH 017/309] #5 chore: application.yml gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 606d42e..0a1ec89 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ +### application.yml ### +applcation.yml + ### STS ### .apt_generated .classpath From 55b4cad31cae402f87c956f1245263c53fa4ce44 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 16:42:45 +0900 Subject: [PATCH 018/309] =?UTF-8?q?#5=20chore:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0a1ec89..3ac2fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ build/ !**/src/test/**/build/ ### application.yml ### -applcation.yml +application.yml ### STS ### .apt_generated From bdc2c3d7ea8e46b0c6d3fa10a0c110da40c5785b Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 20:22:45 +0900 Subject: [PATCH 019/309] =?UTF-8?q?#5=20feat:=20dockerfile=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1437e70 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ + echo "Asia/Seoul" > /etc/timezone +COPY build/libs/*.jar app.jar +CMD ["java", "-jar", "app.jar"] \ No newline at end of file From 4d0f692dbbf32481ed1ddb5a9b2e4d784922ed45 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 5 Apr 2025 21:13:23 +0900 Subject: [PATCH 020/309] =?UTF-8?q?#5=20chor:=20build.gradle=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +++- src/main/resources/application.yml | 0 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 src/main/resources/application.yml diff --git a/build.gradle b/build.gradle index 6eca3fc..31da317 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,8 @@ repositories { } dependencies { + runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' @@ -32,7 +34,7 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - //swagger + // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index e69de29..0000000 From 7d0d64a8ee2b426f97175129777eca3008da4232 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 6 Apr 2025 00:22:25 +0900 Subject: [PATCH 021/309] #5 feat: deploy test --- .github/workflows/deploy.yml | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..0bd0aaa --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,88 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ "main", "develop", "feat/#5-cicd" ] + +jobs: + build-docker-image: + name: Build docker image + runs-on: ubuntu-latest + + steps: + # 1. 코드 체크아웃 + - name: Checkout code + uses: actions/checkout@v3 + + # 2. Java 환경 설정 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + # 3. Gradle Wrapper 실행 권한 추가 + - name: Add execute permission for Gradle Wrapper + run: chmod +x ./gradlew + + - name: Ensure resources directory exists + run: mkdir -p src/main/resources + + # application.yml 파일 생성 + - name: Create application.yml + run: | + cd ./src/main/resources + touch ./application.yml + echo "${{ secrets.APPLICATION_YML }}" > ./application.yml + + # 4. 프로젝트 빌드 + - name: Build with Gradle + uses: gradle/gradle-build-action@v2 + with: + arguments: clean bootJar -Pspring.profiles.active=prod + + # 5. 빌드 결과 확인 (디버깅용) + - name: Check build output + run: ls -la build/libs/ + + # 6. Docker 이미지 빌드 + - name: Build Docker Image + run: docker build -t ${{ secrets.DOCKER_USERNAME }}/closit-app:${{ github.sha }} . + + # 7. Docker Hub 로그인 + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # 8. Docker 이미지 푸시 + - name: Push Docker Image + run: docker push ${{ secrets.DOCKER_USERNAME }}/webeye-server:${{ github.sha }} + + deploy-pipeline: + needs: build-docker-image + runs-on: ubuntu-latest + + steps: + # 1. 코드 체크아웃 + - name: Checkout code + uses: actions/checkout@v3 + + # 2. AWS EC2에 배포 + - name: Deploy to AWS EC2 + uses: appleboy/ssh-action@v0.1.10 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + script: | + docker pull ${{ secrets.DOCKER_USERNAME }}/webeye-server:${{ github.sha }} + docker stop my-app || true + docker rm my-app || true + docker run -d -p 8080:8080 --name my-app \ + -e TZ=Asia/Seoul \ + -e SPRING_DATASOURCE_URL=jdbc:mysql://${{ secrets.RDS_ENDPOINT }}:3306/webeye \ + -e SPRING_DATASOURCE_USERNAME=${{ secrets.RDS_USERNAME }} \ + -e SPRING_DATASOURCE_PASSWORD=${{ secrets.RDS_PASSWORD }} \ + ${{ secrets.DOCKER_USERNAME }}/closit-app:${{ github.sha }} \ No newline at end of file From 7d2824a495c275937da5aa5bd4889afbded48cbd Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 6 Apr 2025 00:26:42 +0900 Subject: [PATCH 022/309] #5 feat: deploy test 2 --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0bd0aaa..58778c2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -47,7 +47,7 @@ jobs: # 6. Docker 이미지 빌드 - name: Build Docker Image - run: docker build -t ${{ secrets.DOCKER_USERNAME }}/closit-app:${{ github.sha }} . + run: docker build -t ${{ secrets.DOCKER_USERNAME }}/webeye-server:${{ github.sha }} . # 7. Docker Hub 로그인 - name: Log in to Docker Hub From 642aadd071b0e92f9a67dc338a6b0f6890a63e98 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 6 Apr 2025 00:29:03 +0900 Subject: [PATCH 023/309] #5 feat: deploy test 3 --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 58778c2..4cfba50 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -85,4 +85,4 @@ jobs: -e SPRING_DATASOURCE_URL=jdbc:mysql://${{ secrets.RDS_ENDPOINT }}:3306/webeye \ -e SPRING_DATASOURCE_USERNAME=${{ secrets.RDS_USERNAME }} \ -e SPRING_DATASOURCE_PASSWORD=${{ secrets.RDS_PASSWORD }} \ - ${{ secrets.DOCKER_USERNAME }}/closit-app:${{ github.sha }} \ No newline at end of file + ${{ secrets.DOCKER_USERNAME }}/webeye-server:${{ github.sha }} \ No newline at end of file From 3721f494c145df9fca6e1351456eb8e43400d0e9 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 6 Apr 2025 01:10:01 +0900 Subject: [PATCH 024/309] =?UTF-8?q?#5=20fix:=20=EB=B8=8C=EB=9E=9C=EC=B9=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4cfba50..081e663 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI/CD Pipeline on: push: - branches: [ "main", "develop", "feat/#5-cicd" ] + branches: [ "main", "develop" ] jobs: build-docker-image: From 214c7274537e9feb3fa95297c9a5e63729c6932f Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 6 Apr 2025 01:15:53 +0900 Subject: [PATCH 025/309] =?UTF-8?q?#5=20docs:=20PR=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pull_request_template.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fcd457d..09ab3fa 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,7 +9,6 @@ ## 📸 스크린샷 -- ## 💬 리뷰 요구사항 From df00a9c991bbf8759b29ea845267a9dd3c44ba06 Mon Sep 17 00:00:00 2001 From: zyovn Date: Mon, 7 Apr 2025 00:05:12 +0900 Subject: [PATCH 026/309] #5 fix: deploy test 4 --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 081e663..4cfba50 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI/CD Pipeline on: push: - branches: [ "main", "develop" ] + branches: [ "main", "develop", "feat/#5-cicd" ] jobs: build-docker-image: From 9ccc3e8816ec62e2aaabe62a16acbefb2bc68119 Mon Sep 17 00:00:00 2001 From: zyovn Date: Mon, 7 Apr 2025 22:13:43 +0900 Subject: [PATCH 027/309] =?UTF-8?q?#5=20fix:=20=EB=B8=8C=EB=9E=9C=EC=B9=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4cfba50..081e663 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI/CD Pipeline on: push: - branches: [ "main", "develop", "feat/#5-cicd" ] + branches: [ "main", "develop" ] jobs: build-docker-image: From 6d243f134e7d60899b26b47c38a03397b8bd1756 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Wed, 9 Apr 2025 00:48:43 +0900 Subject: [PATCH 028/309] =?UTF-8?q?#8=20chore:=20spring=20ai=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 31da317..1fc3437 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ version = '0.0.1-SNAPSHOT' java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } @@ -21,6 +21,19 @@ configurations { repositories { mavenCentral() + maven { + url 'https://repo.spring.io/snapshot' + name = 'Spring Snapshots' + mavenContent { + releasesOnly() + } + } +} + +dependencyManagement { + imports { + mavenBom "org.springframework.ai:spring-ai-bom:1.0.0-M6" + } } dependencies { @@ -36,6 +49,9 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + + //spring ai + implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M6' } tasks.named('test') { From c8f288d26fed2c4d76497d78e779169cde31b25e Mon Sep 17 00:00:00 2001 From: yeonjy Date: Wed, 9 Apr 2025 00:50:09 +0900 Subject: [PATCH 029/309] =?UTF-8?q?#8=20feat:=20OpenAiConfig=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/global/config/OpenAiConfig.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/webeye/backend/global/config/OpenAiConfig.java diff --git a/src/main/java/com/webeye/backend/global/config/OpenAiConfig.java b/src/main/java/com/webeye/backend/global/config/OpenAiConfig.java new file mode 100644 index 0000000..88c4030 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/config/OpenAiConfig.java @@ -0,0 +1,13 @@ +package com.webeye.backend.global.config; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenAiConfig { + @Bean + ChatClient chatClient(ChatClient.Builder builder) { + return builder.build(); + } +} From 10a50c79a6152aa7dd6ff26d63c409ac0003fcf5 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Wed, 9 Apr 2025 00:50:53 +0900 Subject: [PATCH 030/309] #8 docs: swagger title WebEye -> VOIM --- .../java/com/webeye/backend/global/config/SwaggerConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/global/config/SwaggerConfig.java b/src/main/java/com/webeye/backend/global/config/SwaggerConfig.java index d476e73..fcd5b04 100644 --- a/src/main/java/com/webeye/backend/global/config/SwaggerConfig.java +++ b/src/main/java/com/webeye/backend/global/config/SwaggerConfig.java @@ -5,8 +5,8 @@ import io.swagger.v3.oas.annotations.servers.Server; @OpenAPIDefinition( - info = @Info(title = "WebEye API 명세서", - description = "WebEye API 명세서", + info = @Info(title = "VOIM API 명세서", + description = "VOIM API 명세서", version = "v1" ), servers = @Server(url = "/api", description = "Default Server URL") From e0822ae27fbba355aed2f3bc0e40f343a20817e2 Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 9 Apr 2025 01:59:54 +0900 Subject: [PATCH 031/309] =?UTF-8?q?#10=20chore:=20dockerfile=20=EC=9E=90?= =?UTF-8?q?=EB=B0=94=20=EB=B2=84=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1437e70..72fe5a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:17-jdk-slim +FROM openjdk:21-jdk-slim WORKDIR /app RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ echo "Asia/Seoul" > /etc/timezone From 77a6371831f5485c01aa31b4217344d1a0588cdf Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 9 Apr 2025 03:06:42 +0900 Subject: [PATCH 032/309] =?UTF-8?q?#12=20chore:=20deploy.yml=20=EC=9E=90?= =?UTF-8?q?=EB=B0=94=20=EB=B2=84=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 081e663..0c31852 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,10 +15,10 @@ jobs: uses: actions/checkout@v3 # 2. Java 환경 설정 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'temurin' # 3. Gradle Wrapper 실행 권한 추가 From 334cc31c355cfd5414cee749f88d8c06446a7b5e Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 9 Apr 2025 03:07:20 +0900 Subject: [PATCH 033/309] #12 feat: deploy test --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0c31852..b6cb3d3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI/CD Pipeline on: push: - branches: [ "main", "develop" ] + branches: [ "main", "develop", "refactor/#12-deploy" ] jobs: build-docker-image: From 0a510501552cd1f52913249f57511087f8a190b4 Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 9 Apr 2025 03:09:24 +0900 Subject: [PATCH 034/309] =?UTF-8?q?#5=20chore:=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b6cb3d3..0c31852 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI/CD Pipeline on: push: - branches: [ "main", "develop", "refactor/#12-deploy" ] + branches: [ "main", "develop" ] jobs: build-docker-image: From 417eef2d51fc3fe8913927b059bd11e65c3aa52b Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 10 Apr 2025 23:31:24 +0900 Subject: [PATCH 035/309] =?UTF-8?q?#14=20webConfig=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/global/config/WebConfig.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/webeye/backend/global/config/WebConfig.java diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java new file mode 100644 index 0000000..13ea0b2 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -0,0 +1,18 @@ +package com.webeye.backend.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000", "https://voim.store") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } +} From 55d41a468084e786f3699766c54beab067e276f4 Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 16 Apr 2025 17:57:11 +0900 Subject: [PATCH 036/309] =?UTF-8?q?#17=20feat:=20=EC=9B=90=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=20=EC=A3=BC=EC=9D=98=20=EC=98=81=EC=96=91=20=EC=84=B1?= =?UTF-8?q?=EB=B6=84=20Open=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +- .../scheduler/RawMaterialScheduler.java | 20 +++ .../service/RawMaterialService.java | 7 + .../service/RawMaterialServiceImpl.java | 61 ++++++++ .../rawmaterial/domain/RawMaterial.java | 134 ++++++++++++++++++ .../dto/RawMaterialResponseDTO.java | 71 ++++++++++ .../client/RawMaterialClient.java | 26 ++++ .../mapper/RawMaterialMapper.java | 84 +++++++++++ .../persistence/RawMaterialRepository.java | 7 + .../presentation/RawMaterialController.java | 25 ++++ .../global/config/OpenFeignConfig.java | 19 +++ .../backend/global/domain/BaseEntity.java | 24 ++++ .../backend/global/error/ErrorCode.java | 2 + 13 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/application/scheduler/RawMaterialScheduler.java create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/domain/RawMaterial.java create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/dto/RawMaterialResponseDTO.java create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/client/RawMaterialClient.java create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/mapper/RawMaterialMapper.java create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/persistence/RawMaterialRepository.java create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java create mode 100644 src/main/java/com/webeye/backend/global/config/OpenFeignConfig.java create mode 100644 src/main/java/com/webeye/backend/global/domain/BaseEntity.java diff --git a/build.gradle b/build.gradle index 1fc3437..5f9217a 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ repositories { dependencyManagement { imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.2" mavenBom "org.springframework.ai:spring-ai-bom:1.0.0-M6" } } @@ -50,8 +51,11 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' - //spring ai + // spring ai implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M6' + + // open feign + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' } tasks.named('test') { diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/application/scheduler/RawMaterialScheduler.java b/src/main/java/com/webeye/backend/domain/rawmaterial/application/scheduler/RawMaterialScheduler.java new file mode 100644 index 0000000..ab5e6d9 --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/application/scheduler/RawMaterialScheduler.java @@ -0,0 +1,20 @@ +package com.webeye.backend.domain.rawmaterial.application.scheduler; + +import com.webeye.backend.domain.rawmaterial.application.service.RawMaterialService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RawMaterialScheduler { + + private final RawMaterialService rawMaterialService; + + @Scheduled(cron = "0 0 0 1 3 *") + public void updateRawMaterial() { + + } +} diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java b/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java new file mode 100644 index 0000000..8f35f8f --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java @@ -0,0 +1,7 @@ +package com.webeye.backend.domain.rawmaterial.application.service; + +import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; + +public interface RawMaterialService { + RawMaterialResponseDTO.Body getRawMaterial(); +} diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java new file mode 100644 index 0000000..92f9593 --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java @@ -0,0 +1,61 @@ +package com.webeye.backend.domain.rawmaterial.application.service; + +import com.webeye.backend.domain.rawmaterial.infrastructure.mapper.RawMaterialMapper; +import com.webeye.backend.domain.rawmaterial.domain.RawMaterial; +import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.domain.rawmaterial.infrastructure.client.RawMaterialClient; +import com.webeye.backend.domain.rawmaterial.infrastructure.persistence.RawMaterialRepository; +import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.global.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RawMaterialServiceImpl implements RawMaterialService { + + @Value("${open-api.service-key}") + private String serviceKey; + + private final RawMaterialClient rawMaterialClient; + private final RawMaterialRepository rawMaterialRepository; + + @Override + @Transactional + public RawMaterialResponseDTO.Body getRawMaterial() { + RawMaterialResponseDTO response = rawMaterialClient.getRawMaterialInfo(serviceKey, 1, 100, "json"); + + RawMaterialResponseDTO.Response responseDto = response.getResponse(); + + List items = responseDto.getBody().getItems(); + + validateRawMaterial(responseDto, items); + + List rawMaterials = items.stream() + .map(RawMaterialMapper::toEntity) + .collect(Collectors.toList()); + + List saved = rawMaterialRepository.saveAll(rawMaterials); + + return RawMaterialMapper.ofList(saved); + } + + private void validateRawMaterial(RawMaterialResponseDTO.Response response, List items) { + // Open API 응답 실패 + if (response == null || response.getBody() == null) { + throw new BusinessException(ErrorCode.OPEN_API_RESPONSE_NULL); + } + + // items not found + if (items == null || response.getBody().getItems().isEmpty()) { + throw new BusinessException(ErrorCode.OPEN_API_DATA_MISSING); + } + } +} diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/domain/RawMaterial.java b/src/main/java/com/webeye/backend/domain/rawmaterial/domain/RawMaterial.java new file mode 100644 index 0000000..31ddd6f --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/domain/RawMaterial.java @@ -0,0 +1,134 @@ +package com.webeye.backend.domain.rawmaterial.domain; + +import com.webeye.backend.global.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RawMaterial extends BaseEntity { + + @Id + @Column(name = "raw_material_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private String foodCd; + + @Column + private String foodNm; + + @Column + private String nutConSrtrQua; + + @Column + private double enerc; + + @Column + private double water; + + @Column + private double prot; + + @Column + private double fatce; + + @Column + private double ash; + + @Column + private double chocdf; + + @Column + private double sugar; + + @Column + private double fibtg; + + @Column + private double ca; + + @Column + private double fe; + + @Column + private double p; + + @Column + private double k; + + @Column + private double nat; + + @Column + private double vitaRae; + + @Column + private double retol; + + @Column + private double cartb; + + @Column + private double thia; + + @Column + private double ribf; + + @Column + private double nia; + + @Column + private double vitc; + + @Column + private double vitd; + + @Column + private double chole; + + @Column + private double fasat; + + @Column + private double fartn; + + @Builder + private RawMaterial(Long id, String foodCd, String foodNm, String nutConSrtrQua, double enerc, double water, double prot, double fatce, double ash, + double chocdf, double sugar, double fibtg, double ca, double fe, double p, double k, double nat, double vitaRae, double retol, + double cartb, double thia, double ribf, double nia, double vitc, double vitd, double chole, double fasat, double fartn) { + this.id = id; + this.foodCd = foodCd; + this.foodNm = foodNm; + this.nutConSrtrQua = nutConSrtrQua; + this.enerc = enerc; + this.water = water; + this.prot = prot; + this.fatce = fatce; + this.ash = ash; + this.chocdf = chocdf; + this.sugar = sugar; + this.fibtg = fibtg; + this.ca = ca; + this.fe = fe; + this.p = p; + this.k = k; + this.nat = nat; + this.vitaRae = vitaRae; + this.retol = retol; + this.cartb = cartb; + this.thia = thia; + this.ribf = ribf; + this.nia = nia; + this.vitc = vitc; + this.vitd = vitd; + this.chole = chole; + this.fasat = fasat; + this.fartn = fartn; + } +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/dto/RawMaterialResponseDTO.java b/src/main/java/com/webeye/backend/domain/rawmaterial/dto/RawMaterialResponseDTO.java new file mode 100644 index 0000000..0743dd0 --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/dto/RawMaterialResponseDTO.java @@ -0,0 +1,71 @@ +package com.webeye.backend.domain.rawmaterial.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RawMaterialResponseDTO { + + private Response response; + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Response { + private Body body; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Body { + private List items; + private String totalCount; + private String numOfRows; + private String pageNo; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Item { + private Long rawMaterialId; + private String foodCd; + private String foodNm; + private String nutConSrtrQua; + private double enerc; + private double water; + private double prot; + private double fatce; + private double ash; + private double chocdf; + private double sugar; + private double fibtg; + private double ca; + private double fe; + private double p; + private double k; + private double nat; + private double vitaRae; + private double retol; + private double cartb; + private double thia; + private double ribf; + private double nia; + private double vitc; + private double vitd; + private double chole; + private double fasat; + private double fartn; + } +} diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/client/RawMaterialClient.java b/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/client/RawMaterialClient.java new file mode 100644 index 0000000..79630fe --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/client/RawMaterialClient.java @@ -0,0 +1,26 @@ +package com.webeye.backend.domain.rawmaterial.infrastructure.client; + +import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.global.config.OpenFeignConfig; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient( + name = "rawMaterialClient", + url = "${open-api.url}", + configuration = OpenFeignConfig.class +) +public interface RawMaterialClient { + + @GetMapping( + value = "/openapi/tn_pubr_public_nutri_material_info_api", + produces = "application/json" + ) + RawMaterialResponseDTO getRawMaterialInfo( + @RequestParam("serviceKey") String serviceKey, + @RequestParam("pageNo") int pageNo, + @RequestParam("numOfRows") int numOfRows, + @RequestParam("type") String type + ); +} diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/mapper/RawMaterialMapper.java b/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/mapper/RawMaterialMapper.java new file mode 100644 index 0000000..c40e6c9 --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/mapper/RawMaterialMapper.java @@ -0,0 +1,84 @@ +package com.webeye.backend.domain.rawmaterial.infrastructure.mapper; + +import com.webeye.backend.domain.rawmaterial.domain.RawMaterial; +import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; + +import java.util.List; +import java.util.stream.Collectors; + +public class RawMaterialMapper { + + public static RawMaterial toEntity(RawMaterialResponseDTO.Item item) { + return RawMaterial.builder() + .foodCd(item.getFoodCd()) + .foodNm(item.getFoodNm()) + .nutConSrtrQua(item.getNutConSrtrQua()) + .enerc(item.getEnerc()) + .water(item.getWater()) + .prot(item.getProt()) + .fatce(item.getFatce()) + .ash(item.getAsh()) + .chocdf(item.getChocdf()) + .sugar(item.getSugar()) + .fibtg(item.getFibtg()) + .ca(item.getCa()) + .fe(item.getFe()) + .p(item.getP()) + .k(item.getK()) + .nat(item.getNat()) + .vitaRae(item.getVitaRae()) + .retol(item.getRetol()) + .cartb(item.getCartb()) + .thia(item.getThia()) + .ribf(item.getRibf()) + .nia(item.getNia()) + .vitc(item.getVitc()) + .vitd(item.getVitd()) + .chole(item.getChole()) + .fasat(item.getFasat()) + .fartn(item.getFartn()) + .build(); + } + + public static RawMaterialResponseDTO.Item of(RawMaterial rawMaterial) { + return RawMaterialResponseDTO.Item.builder() + .rawMaterialId(rawMaterial.getId()) + .foodCd(rawMaterial.getFoodCd()) + .foodNm(rawMaterial.getFoodNm()) + .nutConSrtrQua(rawMaterial.getNutConSrtrQua()) + .enerc(rawMaterial.getEnerc()) + .water(rawMaterial.getWater()) + .prot(rawMaterial.getProt()) + .fatce(rawMaterial.getFatce()) + .ash(rawMaterial.getAsh()) + .chocdf(rawMaterial.getChocdf()) + .sugar(rawMaterial.getSugar()) + .fibtg(rawMaterial.getFibtg()) + .ca(rawMaterial.getCa()) + .fe(rawMaterial.getFe()) + .p(rawMaterial.getP()) + .k(rawMaterial.getK()) + .nat(rawMaterial.getNat()) + .vitaRae(rawMaterial.getVitaRae()) + .retol(rawMaterial.getRetol()) + .cartb(rawMaterial.getCartb()) + .thia(rawMaterial.getThia()) + .ribf(rawMaterial.getRibf()) + .nia(rawMaterial.getNia()) + .vitc(rawMaterial.getVitc()) + .vitd(rawMaterial.getVitd()) + .chole(rawMaterial.getChole()) + .fasat(rawMaterial.getFasat()) + .fartn(rawMaterial.getFartn()) + .build(); + } + + public static RawMaterialResponseDTO.Body ofList(List rawMaterials) { + List rawMaterialResponseDTOList = rawMaterials.stream() + .map(RawMaterialMapper::of).collect(Collectors.toList()); + + return RawMaterialResponseDTO.Body.builder() + .items(rawMaterialResponseDTOList) + .build(); + } +} diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/persistence/RawMaterialRepository.java b/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/persistence/RawMaterialRepository.java new file mode 100644 index 0000000..cb17bea --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/persistence/RawMaterialRepository.java @@ -0,0 +1,7 @@ +package com.webeye.backend.domain.rawmaterial.infrastructure.persistence; + +import com.webeye.backend.domain.rawmaterial.domain.RawMaterial; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RawMaterialRepository extends JpaRepository { +} diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java b/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java new file mode 100644 index 0000000..230882c --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java @@ -0,0 +1,25 @@ +package com.webeye.backend.domain.rawmaterial.presentation; + +import com.webeye.backend.domain.rawmaterial.application.service.RawMaterialService; +import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "[주의 영양 성분 표시]") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/raw-materials") +public class RawMaterialController { + + private final RawMaterialService rawMaterialService; + + @GetMapping() + public ResponseEntity getRawMaterial() { + RawMaterialResponseDTO.Body response = rawMaterialService.getRawMaterial(); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/webeye/backend/global/config/OpenFeignConfig.java b/src/main/java/com/webeye/backend/global/config/OpenFeignConfig.java new file mode 100644 index 0000000..baa4166 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/config/OpenFeignConfig.java @@ -0,0 +1,19 @@ +package com.webeye.backend.global.config; + +import feign.Logger; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableFeignClients("com.webeye.backend") +@ImportAutoConfiguration(FeignAutoConfiguration.class) +public class OpenFeignConfig { + + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } +} diff --git a/src/main/java/com/webeye/backend/global/domain/BaseEntity.java b/src/main/java/com/webeye/backend/global/domain/BaseEntity.java new file mode 100644 index 0000000..5063f71 --- /dev/null +++ b/src/main/java/com/webeye/backend/global/domain/BaseEntity.java @@ -0,0 +1,24 @@ +package com.webeye.backend.global.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/webeye/backend/global/error/ErrorCode.java b/src/main/java/com/webeye/backend/global/error/ErrorCode.java index 2630ae6..09b7cbf 100644 --- a/src/main/java/com/webeye/backend/global/error/ErrorCode.java +++ b/src/main/java/com/webeye/backend/global/error/ErrorCode.java @@ -7,6 +7,8 @@ @Getter @RequiredArgsConstructor public enum ErrorCode { + OPEN_API_RESPONSE_NULL(HttpStatus.INTERNAL_SERVER_ERROR, "Open API 응답에 실패했습니다."), + OPEN_API_DATA_MISSING(HttpStatus.NOT_FOUND, "Open API 데이터가 존재하지 않습니다.") ; private final HttpStatus status; private final String errorMessage; From e02e42bb14e771f8b8474c7068e9ef4f0808ad9e Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 09:06:22 +0900 Subject: [PATCH 037/309] =?UTF-8?q?#18=20feat:=20OpenAI=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=ED=98=95=EC=8B=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastrucutre/OpenAiClient.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java new file mode 100644 index 0000000..d1c5f02 --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java @@ -0,0 +1,68 @@ +package com.webeye.backend.imageanalysis.infrastrucutre; + +import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Component; +import org.springframework.util.MimeType; + +import java.net.MalformedURLException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OpenAiClient { + private final ChatClient chatClient; + + public AllergyResponse explainAllergy(ImageAnalysisRequest request) { + + String system = """ + You are an assistant that extracts allergy-causing ingredients. + """; + String user = """ + If there is a table in the attached images that describes exactly '원재료명' (ingredients name), check whether any of the ingredients from the example list are present. If they are, return only the names of those ingredients in Korean. Don't use your imagination, and if it's not there, do not say anything. No explanations are needed. Example: 계란, 우유, 메밀, 땅콩, 대두, 밀, 잣, 호두, 게, 새우, 오징어, 고등어, 조개, 복숭아, 토마토, 닭고기, 돼지고기, 쇠고기, 아황산류. Especially, let me know if there is peach. + """; + ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); + return callWithStructuredOutput(request, prompt, AllergyResponse.class); + } + + + private T callWithStructuredOutput(ImageAnalysisRequest request, ImageAnalysisPrompt prompt, Class clazz) { + BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); + + String response = chatClient.prompt() + .user(promptUserSpec -> { + try { + promptUserSpec.text(prompt.user() + outputConverter.getFormat()); + for (String imageUrl : request.urls()) { + MimeType extension = ImageMimeType.fromExtension(extractFileExtension(imageUrl)); + promptUserSpec.media(extension, new UrlResource(imageUrl)); + } + } catch (MalformedURLException exception) { + log.error("MalformedURLException: callWithStructuredOutput() 에서 발생"); + throw new RuntimeException(); + } + }) + .system(prompt.system()) + .call() + .content(); + + return outputConverter.convert(response); + } + + private String extractFileExtension(String url) { + String fileName = url.substring(url.lastIndexOf('/') + 1); + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex == -1) { + return ""; + } + return fileName.substring(dotIndex + 1); + } +} + + From e44c4edc54dce8aaed8ff9d24df470af984436f6 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 09:07:39 +0900 Subject: [PATCH 038/309] =?UTF-8?q?#18=20feat:=20Allergy=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../allergy/application/AllergyService.java | 18 +++++++++++++++ .../allergy/dto/response/AllergyResponse.java | 11 ++++++++++ .../presentation/AllergyController.java | 22 +++++++++++++++++++ .../dto/request/ImageAnalysisRequest.java | 12 ++++++++++ 4 files changed, 63 insertions(+) create mode 100644 src/main/java/com/webeye/backend/allergy/application/AllergyService.java create mode 100644 src/main/java/com/webeye/backend/allergy/dto/response/AllergyResponse.java create mode 100644 src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java create mode 100644 src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java diff --git a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java new file mode 100644 index 0000000..9e9e717 --- /dev/null +++ b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java @@ -0,0 +1,18 @@ +package com.webeye.backend.allergy.application; + +import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.imageanalysis.infrastrucutre.OpenAiClient; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AllergyService { + private final OpenAiClient openAiClient; + + public AllergyResponse analyzeAllergy(ImageAnalysisRequest request) { + return openAiClient.explainAllergy(request); + } + +} diff --git a/src/main/java/com/webeye/backend/allergy/dto/response/AllergyResponse.java b/src/main/java/com/webeye/backend/allergy/dto/response/AllergyResponse.java new file mode 100644 index 0000000..3afec47 --- /dev/null +++ b/src/main/java/com/webeye/backend/allergy/dto/response/AllergyResponse.java @@ -0,0 +1,11 @@ +package com.webeye.backend.allergy.dto.response; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record AllergyResponse ( + List ingredients +) { +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java new file mode 100644 index 0000000..55a5a80 --- /dev/null +++ b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java @@ -0,0 +1,22 @@ +package com.webeye.backend.allergy.presentation; + +import com.webeye.backend.allergy.application.AllergyService; +import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/allergy") +public class AllergyController { + private final AllergyService allergyService; + + @PostMapping(value = "") + public AllergyResponse allergyAnalysis(@RequestBody ImageAnalysisRequest request) { + return allergyService.analyzeAllergy(request); + } +} diff --git a/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java b/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java new file mode 100644 index 0000000..4d226ad --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java @@ -0,0 +1,12 @@ +package com.webeye.backend.imageanalysis.dto.request; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record ImageAnalysisRequest ( + List urls +) { +} + From 646631e256f55607923c884c3e82712beb66eab7 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 09:08:42 +0900 Subject: [PATCH 039/309] =?UTF-8?q?#18=20feat:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=98=95=EC=8B=9D=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastrucutre/ImageMimeType.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/ImageMimeType.java diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/ImageMimeType.java b/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/ImageMimeType.java new file mode 100644 index 0000000..b2aaa68 --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/ImageMimeType.java @@ -0,0 +1,40 @@ +package com.webeye.backend.imageanalysis.infrastrucutre; + +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum ImageMimeType { + PNG("png", MimeTypeUtils.IMAGE_PNG), + JPEG("jpeg", MimeTypeUtils.IMAGE_JPEG), + JPG("jpg", MimeTypeUtils.IMAGE_JPEG), + GIF("gif", MimeTypeUtils.IMAGE_GIF), + WEBP("webp", MimeTypeUtils.parseMimeType("image/webp")); + + private final String extension; + private final MimeType mimeType; + + private static final Map EXTENSION_TO_MIMETYPE_MAP; + + static { + EXTENSION_TO_MIMETYPE_MAP = Arrays.stream(values()) + .collect(Collectors.toMap(type -> type.extension, type -> type.mimeType)); + } + + ImageMimeType(String extension, MimeType mimeType) { + this.extension = extension; + this.mimeType = mimeType; + } + + public static MimeType fromExtension(String extension) { + String lowerExtension = extension.toLowerCase(); + MimeType mimeType = EXTENSION_TO_MIMETYPE_MAP.get(lowerExtension); + if (mimeType == null) { + throw new IllegalArgumentException("Unsupported image extension: " + extension); + } + return mimeType; + } +} \ No newline at end of file From 52e27a059581314accf133aa23a19cca1f96922a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 09:09:35 +0900 Subject: [PATCH 040/309] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../imageanalysis/dto/request/ImageAnalysisPrompt.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisPrompt.java diff --git a/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisPrompt.java b/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisPrompt.java new file mode 100644 index 0000000..e17f965 --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisPrompt.java @@ -0,0 +1,7 @@ +package com.webeye.backend.imageanalysis.dto.request; + +public record ImageAnalysisPrompt ( + String system, + String user +) { +} From a7ed37b4096e90d0ab202199113ea4d879c5cec5 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 13:27:09 +0900 Subject: [PATCH 041/309] =?UTF-8?q?#18=20feat:=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EA=B4=80=EB=A0=A8=20Prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastrucutre/OpenAiClient.java | 64 ++++++++++++++++++- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java index d1c5f02..23117d7 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java @@ -1,6 +1,8 @@ package com.webeye.backend.imageanalysis.infrastrucutre; import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; +import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; import lombok.RequiredArgsConstructor; @@ -19,14 +21,69 @@ public class OpenAiClient { private final ChatClient chatClient; - public AllergyResponse explainAllergy(ImageAnalysisRequest request) { + public PointExplanationResponse explainProductPoint(ImageAnalysisRequest request) { + String system = """ + You are an expert in analyzing product images and extracting key information that helps users make purchase decisions. + Based on the image provided, extract only the most relevant and concise information that highlights the product's core value. + The extracted list should contain no more than 6 items. Do not include unnecessary or overly detailed information—only what's essential for a buyer. + """; + String user = """ + Analyze the image and extract key product information in the format below. + You may adjust the content depending on the product type, but make sure the extracted items are helpful for making a purchase decision. + Limit the result to a maximum of 6 items, and keep each one clear and concise. + Below is just a guideline — instead of following the content exactly, follow the intent. + + Example (Generate appropriately according to the product) + - 핵심 특징 요약 (제품 포인트) + - 당류 비교 (10g당 당류 함량 비교) + - 제품 추천 대상 + - 상세 원재료 + - 영양 정보 (1개 10g 기준) + """; + + ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); + return callWithStructuredOutput(request, prompt, PointExplanationResponse.class); + } + + + public DetailExplanationResponse explainProductDetail(ImageAnalysisRequest request) { + String system = """ + You are an expert in providing detailed explanations about products based on images. + When a user provides a product description image along with the key elements of that description, you should offer a clear and detailed explanation of that element. + In this explanation, you must provide very detailed information about that element from the image. + """; + + String user = """ + I have provided a product description image along with the key descriptive elements extracted from the image. + Please generate a detailed explanation of the provided key descriptive element. + """; + + ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); + return callWithStructuredOutput(request, prompt, DetailExplanationResponse.class); + } + + public AllergyResponse explainAllergy(ImageAnalysisRequest request) { String system = """ You are an assistant that extracts allergy-causing ingredients. """; + String user = """ - If there is a table in the attached images that describes exactly '원재료명' (ingredients name), check whether any of the ingredients from the example list are present. If they are, return only the names of those ingredients in Korean. Don't use your imagination, and if it's not there, do not say anything. No explanations are needed. Example: 계란, 우유, 메밀, 땅콩, 대두, 밀, 잣, 호두, 게, 새우, 오징어, 고등어, 조개, 복숭아, 토마토, 닭고기, 돼지고기, 쇠고기, 아황산류. Especially, let me know if there is peach. - """; + Step 1: Carefully examine the attached image(s). + If there is a table that clearly describes '원재료명' (ingredients name), identify it. + Do not output anything unless the table exists. + + Step 2: From the '원재료명' table, extract all listed ingredients. Do not output anything yet. + + Step 3: Compare the extracted ingredients with the following list: + 계란, 우유, 메밀, 땅콩, 대두, 밀, 잣, 호두, 게, 새우, 오징어, 고등어, 조개, 복숭아, 토마토, 닭고기, 돼지고기, 쇠고기, 아황산류. + + Step 4: If any ingredients from the list are found in the table, return only those names **in Korean**. + Do not explain. If none are found, return nothing. + + Step 5: If '복숭아' is present, be sure to include it in the result — even if it's the only match. + """; + ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); return callWithStructuredOutput(request, prompt, AllergyResponse.class); } @@ -63,6 +120,7 @@ private String extractFileExtension(String url) { } return fileName.substring(dotIndex + 1); } + } From 7e625d9eebb37530b88d212257f85f45430a0c79 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 13:27:47 +0900 Subject: [PATCH 042/309] =?UTF-8?q?#18=20feat:=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=A3=BC=EC=9A=94=20=EC=9A=94=EC=86=8C=20?= =?UTF-8?q?&=20=EC=83=81=EC=84=B8=20=EC=84=A4=EB=AA=85=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ExplanationService.java | 22 +++++++++++++++ .../presentation/ExplanationController.java | 28 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/main/java/com/webeye/backend/explanation/application/ExplanationService.java create mode 100644 src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java diff --git a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java new file mode 100644 index 0000000..09311bb --- /dev/null +++ b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java @@ -0,0 +1,22 @@ +package com.webeye.backend.explanation.application; + +import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; +import com.webeye.backend.explanation.dto.response.PointExplanationResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.imageanalysis.infrastrucutre.OpenAiClient; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ExplanationService { + private final OpenAiClient openAiClient; + + public PointExplanationResponse analyzeProductPoint(ImageAnalysisRequest request) { + return openAiClient.explainProductPoint(request); + } + + public DetailExplanationResponse analyzeProductDetail(ImageAnalysisRequest request) { + return openAiClient.explainProductDetail(request); + } +} diff --git a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java new file mode 100644 index 0000000..e66c3ab --- /dev/null +++ b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java @@ -0,0 +1,28 @@ +package com.webeye.backend.explanation.presentation; + +import com.webeye.backend.explanation.application.ExplanationService; +import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; +import com.webeye.backend.explanation.dto.response.PointExplanationResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/explanation") +public class ExplanationController { + private final ExplanationService explanationService; + + @PostMapping(value = "") + public PointExplanationResponse productAnalysis(@RequestBody ImageAnalysisRequest request) { + return explanationService.analyzeProductPoint(request); + } + + @PostMapping(value = "detail") + public DetailExplanationResponse productDetailAnalysis(@RequestBody ImageAnalysisRequest request) { + return explanationService.analyzeProductDetail(request); + } +} From 132648252085b2e2585b2d1df122e3b5edfb59aa Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 13:28:06 +0900 Subject: [PATCH 043/309] =?UTF-8?q?#18=20feat:=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EA=B4=80=EB=A0=A8=20response=20dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/DetailExplanationResponse.java | 9 +++++++++ .../dto/response/PointExplanationResponse.java | 11 +++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/main/java/com/webeye/backend/explanation/dto/response/DetailExplanationResponse.java create mode 100644 src/main/java/com/webeye/backend/explanation/dto/response/PointExplanationResponse.java diff --git a/src/main/java/com/webeye/backend/explanation/dto/response/DetailExplanationResponse.java b/src/main/java/com/webeye/backend/explanation/dto/response/DetailExplanationResponse.java new file mode 100644 index 0000000..4e4471c --- /dev/null +++ b/src/main/java/com/webeye/backend/explanation/dto/response/DetailExplanationResponse.java @@ -0,0 +1,9 @@ +package com.webeye.backend.explanation.dto.response; + +import lombok.Builder; + +@Builder +public record DetailExplanationResponse ( + String detail +) { +} diff --git a/src/main/java/com/webeye/backend/explanation/dto/response/PointExplanationResponse.java b/src/main/java/com/webeye/backend/explanation/dto/response/PointExplanationResponse.java new file mode 100644 index 0000000..d8324dd --- /dev/null +++ b/src/main/java/com/webeye/backend/explanation/dto/response/PointExplanationResponse.java @@ -0,0 +1,11 @@ +package com.webeye.backend.explanation.dto.response; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record PointExplanationResponse( + List keyFeatures +) { +} From bca8db0bfbb46a23ba8479f2dc60495afcdc31e7 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 13:37:48 +0900 Subject: [PATCH 044/309] =?UTF-8?q?#18=20feat:=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=98=81=EC=96=91=EC=86=8C=20=EC=B6=94=EC=B6=9C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/NutritionService.java | 17 ++++++++++++++ .../presentation/NutritionController.java | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/com/webeye/backend/nutrition/application/NutritionService.java create mode 100644 src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java new file mode 100644 index 0000000..70582e7 --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -0,0 +1,17 @@ +package com.webeye.backend.nutrition.application; + +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.imageanalysis.infrastrucutre.OpenAiClient; +import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class NutritionService { + private final OpenAiClient openAiClient; + + public NutritionResponse analyzeNutrition(ImageAnalysisRequest request) { + return openAiClient.explainNutrition(request); + } +} diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java new file mode 100644 index 0000000..05722b0 --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java @@ -0,0 +1,22 @@ +package com.webeye.backend.nutrition.presentation; + +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.nutrition.application.NutritionService; +import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/nutrition") +public class NutritionController { + private final NutritionService nutritionService; + + @PostMapping(value = "") + public NutritionResponse nutritionAnalysis(@RequestBody ImageAnalysisRequest request) { + return nutritionService.analyzeNutrition(request); + } +} From 04168da59999462f5300ff772a493fe92f97f2cf Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 13:56:39 +0900 Subject: [PATCH 045/309] =?UTF-8?q?#18=20feat:=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=98=81=EC=96=91=EC=86=8C=20=EC=B6=94=EC=B6=9C=20=ED=94=84?= =?UTF-8?q?=EB=A1=AC=ED=94=84=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastrucutre/OpenAiClient.java | 14 +++++++++++++ .../dto/response/NutritionResponse.java | 21 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java index 23117d7..d232b06 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java @@ -5,6 +5,7 @@ import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.nutrition.dto.response.NutritionResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; @@ -88,6 +89,19 @@ public AllergyResponse explainAllergy(ImageAnalysisRequest request) { return callWithStructuredOutput(request, prompt, AllergyResponse.class); } + public NutritionResponse explainNutrition(ImageAnalysisRequest request) { + String system = """ + You are a nutrition description assistant. + """; + + String user = """ + If the attached images contain 'nutrition information', please provide the amount of each nutrient in the format I sent. + """; + + ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); + return callWithStructuredOutput(request, prompt, NutritionResponse.class); + } + private T callWithStructuredOutput(ImageAnalysisRequest request, ImageAnalysisPrompt prompt, Class clazz) { BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); diff --git a/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java new file mode 100644 index 0000000..1a0a6d3 --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java @@ -0,0 +1,21 @@ +package com.webeye.backend.nutrition.dto.response; + +import lombok.Builder; + +@Builder +public record NutritionResponse ( + Double sodium, + Double carbohydrate, + Double sugars, + Double fat, + Double transFat, + Double saturatedFat, + Double cholesterol, + Double protein, + Double calcium, + Double phosphorus, + Double niacin, + Double vitaminB, + Double vitaminE +) { +} \ No newline at end of file From 1150d3002afc90451ee2331c7866b4c43c7ac619 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 13:57:23 +0900 Subject: [PATCH 046/309] =?UTF-8?q?#18=20feat:=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=98=81=EC=96=91=EC=86=8C=20=EC=B6=94=EC=B6=9C=20API=EC=9D=98?= =?UTF-8?q?=20swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/dto/response/type/SuccessCode.java | 5 ++++ .../presentation/NutritionController.java | 17 +++++++----- .../swagger/NutritionSwagger.java | 27 +++++++++++++++++++ 3 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index d19d0ac..3d86aa8 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -9,6 +9,11 @@ public enum SuccessCode { // health HEALTH_CHECK_SUCCESS(200, "Health Check Success"), + + // nutrition + NUTRITION_ANALYSIS_SUCCESS(201, "Nutrition analysis success"), + + ; private final int status; diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java index 05722b0..cd6f539 100644 --- a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java +++ b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java @@ -1,22 +1,25 @@ package com.webeye.backend.nutrition.presentation; +import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; import com.webeye.backend.nutrition.application.NutritionService; import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import com.webeye.backend.nutrition.presentation.swagger.NutritionSwagger; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.NUTRITION_ANALYSIS_SUCCESS; @RequiredArgsConstructor @RestController @RequestMapping("/nutrition") -public class NutritionController { +public class NutritionController implements NutritionSwagger { private final NutritionService nutritionService; + @ResponseStatus(HttpStatus.CREATED) @PostMapping(value = "") - public NutritionResponse nutritionAnalysis(@RequestBody ImageAnalysisRequest request) { - return nutritionService.analyzeNutrition(request); + public SuccessResponse nutritionAnalysis(@RequestBody ImageAnalysisRequest request) { + return SuccessResponse.of(NUTRITION_ANALYSIS_SUCCESS, nutritionService.analyzeNutrition(request)); } } diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java b/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java new file mode 100644 index 0000000..01de9c1 --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java @@ -0,0 +1,27 @@ +package com.webeye.backend.nutrition.presentation.swagger; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "Nutrition", description = "제품 영양소 관련 API") +public interface NutritionSwagger { + @Operation( + summary = "제품의 영양소 함량 추출", + description = "음식 제품에 대한 영양소 함량을 추출합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "영양소 함량이 성공적으로 추출되었습니다." + ) + }) + SuccessResponse nutritionAnalysis( + @RequestBody ImageAnalysisRequest request + ); +} From 4f2e117d60a108ef57f738bbc38cf0988445209a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 14:31:26 +0900 Subject: [PATCH 047/309] =?UTF-8?q?#18=20feat:=20=EC=95=8C=EB=9F=AC?= =?UTF-8?q?=EC=A7=80=20=EC=9C=A0=EB=B0=9C=20=EC=84=B1=EB=B6=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B3=B5=20API=EC=9D=98=20swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/AllergyController.java | 14 +++++---- .../presentation/swagger/AllergySwagger.java | 29 +++++++++++++++++++ .../global/dto/response/type/SuccessCode.java | 2 ++ 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java diff --git a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java index 55a5a80..1a92e5f 100644 --- a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java +++ b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java @@ -2,12 +2,13 @@ import com.webeye.backend.allergy.application.AllergyService; import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.ALLERGY_ANALYSIS_SUCCESS; @RequiredArgsConstructor @RestController @@ -15,8 +16,9 @@ public class AllergyController { private final AllergyService allergyService; + @ResponseStatus(HttpStatus.CREATED) @PostMapping(value = "") - public AllergyResponse allergyAnalysis(@RequestBody ImageAnalysisRequest request) { - return allergyService.analyzeAllergy(request); + public SuccessResponse allergyAnalysis(@RequestBody ImageAnalysisRequest request) { + return SuccessResponse.of(ALLERGY_ANALYSIS_SUCCESS ,allergyService.analyzeAllergy(request)); } } diff --git a/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java b/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java new file mode 100644 index 0000000..5aa1926 --- /dev/null +++ b/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java @@ -0,0 +1,29 @@ +package com.webeye.backend.allergy.presentation.swagger; + +import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "Allergy", description = "제품 알러지 유발 성분 관련 API") +public interface AllergySwagger { + @Operation( + summary = "제품의 알러지 유발 성분 추출", + description = "음식 제품에 대한 알러지 유발 성분을 추출합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "알러지 유발 요소가 성공적으로 추출되었습니다." + ) + }) + SuccessResponse allergyAnalysis( + @RequestBody ImageAnalysisRequest request + ); + +} diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index 3d86aa8..c0419bc 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -13,6 +13,8 @@ public enum SuccessCode { // nutrition NUTRITION_ANALYSIS_SUCCESS(201, "Nutrition analysis success"), + // allergy + ALLERGY_ANALYSIS_SUCCESS(201, "Allergy analysis success"), ; From a33ca9002a8ba7ce5666b4228c7603f12581aa1c Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 14:35:17 +0900 Subject: [PATCH 048/309] #18 feat: swagger method override --- .../backend/allergy/presentation/AllergyController.java | 4 +++- .../backend/nutrition/presentation/NutritionController.java | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java index 1a92e5f..204a897 100644 --- a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java +++ b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java @@ -2,6 +2,7 @@ import com.webeye.backend.allergy.application.AllergyService; import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.allergy.presentation.swagger.AllergySwagger; import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; import lombok.RequiredArgsConstructor; @@ -13,9 +14,10 @@ @RequiredArgsConstructor @RestController @RequestMapping("/allergy") -public class AllergyController { +public class AllergyController implements AllergySwagger { private final AllergyService allergyService; + @Override @ResponseStatus(HttpStatus.CREATED) @PostMapping(value = "") public SuccessResponse allergyAnalysis(@RequestBody ImageAnalysisRequest request) { diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java index cd6f539..90913c5 100644 --- a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java +++ b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java @@ -17,6 +17,7 @@ public class NutritionController implements NutritionSwagger { private final NutritionService nutritionService; + @Override @ResponseStatus(HttpStatus.CREATED) @PostMapping(value = "") public SuccessResponse nutritionAnalysis(@RequestBody ImageAnalysisRequest request) { From 906e0839f51113354dd9d19d9d3dc02ee5fc94ed Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 17 Apr 2025 14:42:06 +0900 Subject: [PATCH 049/309] =?UTF-8?q?#18=20feat:=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20API=EC=9D=98=20swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ExplanationController.java | 27 +++++++----- .../swagger/ExplanationSwagger.java | 44 +++++++++++++++++++ .../global/dto/response/type/SuccessCode.java | 4 ++ 3 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java diff --git a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java index e66c3ab..c0c39eb 100644 --- a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java +++ b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java @@ -3,26 +3,33 @@ import com.webeye.backend.explanation.application.ExplanationService; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; +import com.webeye.backend.explanation.presentation.swagger.ExplanationSwagger; +import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS; +import static com.webeye.backend.global.dto.response.type.SuccessCode.PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS; @RequiredArgsConstructor @RestController @RequestMapping("/explanation") -public class ExplanationController { +public class ExplanationController implements ExplanationSwagger { private final ExplanationService explanationService; + @Override + @ResponseStatus(HttpStatus.CREATED) @PostMapping(value = "") - public PointExplanationResponse productAnalysis(@RequestBody ImageAnalysisRequest request) { - return explanationService.analyzeProductPoint(request); + public SuccessResponse productAnalysis(@RequestBody ImageAnalysisRequest request) { + return SuccessResponse.of(PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS, explanationService.analyzeProductPoint(request)); } - @PostMapping(value = "detail") - public DetailExplanationResponse productDetailAnalysis(@RequestBody ImageAnalysisRequest request) { - return explanationService.analyzeProductDetail(request); + @Override + @ResponseStatus(HttpStatus.CREATED) + @PostMapping(value = "/detail") + public SuccessResponse productDetailAnalysis(@RequestBody ImageAnalysisRequest request) { + return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, explanationService.analyzeProductDetail(request)); } } diff --git a/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java b/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java new file mode 100644 index 0000000..5565aeb --- /dev/null +++ b/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java @@ -0,0 +1,44 @@ +package com.webeye.backend.explanation.presentation.swagger; + +import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; +import com.webeye.backend.explanation.dto.response.PointExplanationResponse; +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "Explanation", description = "제품 설명 관련 API") +public interface ExplanationSwagger { + @Operation( + summary = "제품 설명 이미지에서 주요 요소 추출", + description = "제품 설명 이미지를 입력받아 주요 설명 요소를 추출합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "제품 주요 설명 요소가 추출되었습니다." + ) + }) + SuccessResponse productAnalysis( + @RequestBody ImageAnalysisRequest request + ); + + @Operation( + summary = "제품 주요 요소에 대한 상세 설명 추출", + description = "제품 설명 이미지를 입력받아 주요 요소에 대한 상세 설명을 추출합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "제품 주요 요소에 대한 상세 설명이 추출되었습니다." + ) + }) + SuccessResponse productDetailAnalysis( + @RequestBody ImageAnalysisRequest request + ); +} + diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index c0419bc..12a334a 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -16,6 +16,10 @@ public enum SuccessCode { // allergy ALLERGY_ANALYSIS_SUCCESS(201, "Allergy analysis success"), + // explanation + PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS(201, "Product point explanation analysis success"), + PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS(201, "Product detail explanation analysis success"), + ; private final int status; From 2ed6f432c759c65cee57521f21a6a9177de606b9 Mon Sep 17 00:00:00 2001 From: yeonjy <81320703+yeonjy@users.noreply.github.com> Date: Thu, 17 Apr 2025 23:34:34 +0900 Subject: [PATCH 050/309] Create .coderabbit.yaml --- .coderabbit.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..43f56b8 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,14 @@ +language: "ko-KR" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: false +chat: + auto_reply: true From ac89ccb677af3cc398eac1ba57ec5e230b2291a9 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 18 Apr 2025 00:45:37 +0900 Subject: [PATCH 051/309] =?UTF-8?q?#17=20refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=ED=98=95=EC=8B=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RawMaterialService.java | 2 +- .../service/RawMaterialServiceImpl.java | 2 +- .../presentation/RawMaterialController.java | 16 ++++++------- .../swagger/RawMaterialSwagger.java | 23 +++++++++++++++++++ .../global/dto/response/type/SuccessCode.java | 3 +++ 5 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/presentation/swagger/RawMaterialSwagger.java diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java b/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java index 8f35f8f..1407a3a 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java @@ -3,5 +3,5 @@ import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; public interface RawMaterialService { - RawMaterialResponseDTO.Body getRawMaterial(); + RawMaterialResponseDTO.Body callRawMaterialAPI(); } diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java index 92f9593..6bec43b 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java @@ -29,7 +29,7 @@ public class RawMaterialServiceImpl implements RawMaterialService { @Override @Transactional - public RawMaterialResponseDTO.Body getRawMaterial() { + public RawMaterialResponseDTO.Body callRawMaterialAPI() { RawMaterialResponseDTO response = rawMaterialClient.getRawMaterialInfo(serviceKey, 1, 100, "json"); RawMaterialResponseDTO.Response responseDto = response.getResponse(); diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java b/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java index 230882c..3ffa5d9 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java @@ -2,24 +2,24 @@ import com.webeye.backend.domain.rawmaterial.application.service.RawMaterialService; import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; -import io.swagger.v3.oas.annotations.tags.Tag; +import com.webeye.backend.domain.rawmaterial.presentation.swagger.RawMaterialSwagger; +import com.webeye.backend.global.dto.response.SuccessResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "[주의 영양 성분 표시]") +import static com.webeye.backend.global.dto.response.type.SuccessCode.RAW_MATERIAL_API_SUCCESS; + @RestController @RequiredArgsConstructor @RequestMapping("/v1/raw-materials") -public class RawMaterialController { +public class RawMaterialController implements RawMaterialSwagger { private final RawMaterialService rawMaterialService; - @GetMapping() - public ResponseEntity getRawMaterial() { - RawMaterialResponseDTO.Body response = rawMaterialService.getRawMaterial(); - return ResponseEntity.ok(response); + @GetMapping + public SuccessResponse callRawMaterialApi() { + return SuccessResponse.of(RAW_MATERIAL_API_SUCCESS, rawMaterialService.callRawMaterialAPI()); } } diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/swagger/RawMaterialSwagger.java b/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/swagger/RawMaterialSwagger.java new file mode 100644 index 0000000..b09ae72 --- /dev/null +++ b/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/swagger/RawMaterialSwagger.java @@ -0,0 +1,23 @@ +package com.webeye.backend.domain.rawmaterial.presentation.swagger; + +import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.global.dto.response.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "[원재료 주의 영양 성분]", description = "원재료 주의 영양 성분 표시 관련 API") +public interface RawMaterialSwagger { + @Operation( + summary = "원재료 영양 성분 OPEN API 호출", + description = "공공 데이터 포털 원재료 영양 성분 OPEN API를 호출합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OPEN API가 성공적으로 호출되었습니다." + ) + }) + SuccessResponse callRawMaterialApi(); +} diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index d19d0ac..60aa0fc 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -9,6 +9,9 @@ public enum SuccessCode { // health HEALTH_CHECK_SUCCESS(200, "Health Check Success"), + + // raw material + RAW_MATERIAL_API_SUCCESS(200, "Raw Material API Success"), ; private final int status; From e2ea9bb772bfa97632f4cc832c563ce34d31a740 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 19 Apr 2025 00:41:03 +0900 Subject: [PATCH 052/309] =?UTF-8?q?#18=20rename:=20infrastructure=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=EB=AA=85=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/allergy/application/AllergyService.java | 2 +- .../explanation/application/ExplanationService.java | 2 +- .../ImageMimeType.java | 2 +- .../OpenAiClient.java | 10 +++++++--- .../nutrition/application/NutritionService.java | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) rename src/main/java/com/webeye/backend/imageanalysis/{infrastrucutre => infrastructure}/ImageMimeType.java (95%) rename src/main/java/com/webeye/backend/imageanalysis/{infrastrucutre => infrastructure}/OpenAiClient.java (94%) diff --git a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java index 9e9e717..46b6e05 100644 --- a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java +++ b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java @@ -2,7 +2,7 @@ import com.webeye.backend.allergy.dto.response.AllergyResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; -import com.webeye.backend.imageanalysis.infrastrucutre.OpenAiClient; +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java index 09311bb..b938ca8 100644 --- a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java +++ b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java @@ -3,7 +3,7 @@ import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; -import com.webeye.backend.imageanalysis.infrastrucutre.OpenAiClient; +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/ImageMimeType.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageMimeType.java similarity index 95% rename from src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/ImageMimeType.java rename to src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageMimeType.java index b2aaa68..13186f3 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/ImageMimeType.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageMimeType.java @@ -1,4 +1,4 @@ -package com.webeye.backend.imageanalysis.infrastrucutre; +package com.webeye.backend.imageanalysis.infrastructure; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java similarity index 94% rename from src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java rename to src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index d232b06..a6cc788 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastrucutre/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -1,8 +1,9 @@ -package com.webeye.backend.imageanalysis.infrastrucutre; +package com.webeye.backend.imageanalysis.infrastructure; import com.webeye.backend.allergy.dto.response.AllergyResponse; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; +import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionResponse; @@ -16,6 +17,9 @@ import java.net.MalformedURLException; +import static com.webeye.backend.global.error.ErrorCode.FILE_EXTENSION_NOT_FOUND; +import static com.webeye.backend.global.error.ErrorCode.INVALID_IMAGE_URL; + @Slf4j @Component @RequiredArgsConstructor @@ -116,7 +120,7 @@ private T callWithStructuredOutput(ImageAnalysisRequest request, ImageAnalys } } catch (MalformedURLException exception) { log.error("MalformedURLException: callWithStructuredOutput() 에서 발생"); - throw new RuntimeException(); + throw new BusinessException(INVALID_IMAGE_URL); } }) .system(prompt.system()) @@ -130,7 +134,7 @@ private String extractFileExtension(String url) { String fileName = url.substring(url.lastIndexOf('/') + 1); int dotIndex = fileName.lastIndexOf('.'); if (dotIndex == -1) { - return ""; + throw new BusinessException(FILE_EXTENSION_NOT_FOUND); } return fileName.substring(dotIndex + 1); } diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index 70582e7..56f496f 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -1,7 +1,7 @@ package com.webeye.backend.nutrition.application; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; -import com.webeye.backend.imageanalysis.infrastrucutre.OpenAiClient; +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.nutrition.dto.response.NutritionResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; From 98fb1a5d3ca821aa59f23b02e614be079b040d2f Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 19 Apr 2025 00:41:45 +0900 Subject: [PATCH 053/309] #18: HttpStatus CREATED -> OK --- .../backend/allergy/presentation/AllergyController.java | 5 +++-- .../explanation/presentation/ExplanationController.java | 9 +++++---- .../backend/global/dto/response/type/SuccessCode.java | 8 ++++---- .../nutrition/presentation/NutritionController.java | 5 +++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java index 204a897..4ae6d8a 100644 --- a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java +++ b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java @@ -5,6 +5,7 @@ import com.webeye.backend.allergy.presentation.swagger.AllergySwagger; import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -18,9 +19,9 @@ public class AllergyController implements AllergySwagger { private final AllergyService allergyService; @Override - @ResponseStatus(HttpStatus.CREATED) + @ResponseStatus(HttpStatus.OK) @PostMapping(value = "") - public SuccessResponse allergyAnalysis(@RequestBody ImageAnalysisRequest request) { + public SuccessResponse allergyAnalysis(@Valid @RequestBody ImageAnalysisRequest request) { return SuccessResponse.of(ALLERGY_ANALYSIS_SUCCESS ,allergyService.analyzeAllergy(request)); } } diff --git a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java index c0c39eb..087054f 100644 --- a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java +++ b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java @@ -6,6 +6,7 @@ import com.webeye.backend.explanation.presentation.swagger.ExplanationSwagger; import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -20,16 +21,16 @@ public class ExplanationController implements ExplanationSwagger { private final ExplanationService explanationService; @Override - @ResponseStatus(HttpStatus.CREATED) + @ResponseStatus(HttpStatus.OK) @PostMapping(value = "") - public SuccessResponse productAnalysis(@RequestBody ImageAnalysisRequest request) { + public SuccessResponse productAnalysis(@Valid @RequestBody ImageAnalysisRequest request) { return SuccessResponse.of(PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS, explanationService.analyzeProductPoint(request)); } @Override - @ResponseStatus(HttpStatus.CREATED) + @ResponseStatus(HttpStatus.OK) @PostMapping(value = "/detail") - public SuccessResponse productDetailAnalysis(@RequestBody ImageAnalysisRequest request) { + public SuccessResponse productDetailAnalysis(@Valid @RequestBody ImageAnalysisRequest request) { return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, explanationService.analyzeProductDetail(request)); } } diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index 12a334a..7fd8bf1 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -11,14 +11,14 @@ public enum SuccessCode { HEALTH_CHECK_SUCCESS(200, "Health Check Success"), // nutrition - NUTRITION_ANALYSIS_SUCCESS(201, "Nutrition analysis success"), + NUTRITION_ANALYSIS_SUCCESS(200, "Nutrition analysis success"), // allergy - ALLERGY_ANALYSIS_SUCCESS(201, "Allergy analysis success"), + ALLERGY_ANALYSIS_SUCCESS(200, "Allergy analysis success"), // explanation - PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS(201, "Product point explanation analysis success"), - PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS(201, "Product detail explanation analysis success"), + PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS(200, "Product point explanation analysis success"), + PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS(200, "Product detail explanation analysis success"), ; diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java index 90913c5..f943dd1 100644 --- a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java +++ b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java @@ -5,6 +5,7 @@ import com.webeye.backend.nutrition.application.NutritionService; import com.webeye.backend.nutrition.dto.response.NutritionResponse; import com.webeye.backend.nutrition.presentation.swagger.NutritionSwagger; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -18,9 +19,9 @@ public class NutritionController implements NutritionSwagger { private final NutritionService nutritionService; @Override - @ResponseStatus(HttpStatus.CREATED) + @ResponseStatus(HttpStatus.OK) @PostMapping(value = "") - public SuccessResponse nutritionAnalysis(@RequestBody ImageAnalysisRequest request) { + public SuccessResponse nutritionAnalysis(@Valid @RequestBody ImageAnalysisRequest request) { return SuccessResponse.of(NUTRITION_ANALYSIS_SUCCESS, nutritionService.analyzeNutrition(request)); } } From d11d8f955a1ae4d72f291da1e28ce2299d6da9a9 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 19 Apr 2025 00:42:18 +0900 Subject: [PATCH 054/309] =?UTF-8?q?#18=20docs:=20DTO=20Schema=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ImageAnalysisRequest.java | 4 ++++ .../dto/response/NutritionResponse.java | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java b/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java index 4d226ad..0fc5eec 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java @@ -1,11 +1,15 @@ package com.webeye.backend.imageanalysis.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; import lombok.Builder; import java.util.List; +@Schema(description = "이미지 분석 URL 요청") @Builder public record ImageAnalysisRequest ( + @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") List urls ) { } diff --git a/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java index 1a0a6d3..99e29c4 100644 --- a/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java +++ b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java @@ -1,21 +1,36 @@ package com.webeye.backend.nutrition.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; +@Schema(description = "제품 영양 정보 응답") @Builder -public record NutritionResponse ( +public record NutritionResponse( + @Schema(description = "나트륨 (mg)", example = "120.5") Double sodium, + @Schema(description = "탄수화물 (g)", example = "25.0") Double carbohydrate, + @Schema(description = "당류 (g)", example = "10.2") Double sugars, + @Schema(description = "지방 (g)", example = "5.5") Double fat, + @Schema(description = "트랜스지방 (g)", example = "0.0") Double transFat, + @Schema(description = "포화지방 (g)", example = "1.2") Double saturatedFat, + @Schema(description = "콜레스테롤 (mg)", example = "10.0") Double cholesterol, + @Schema(description = "단백질 (g)", example = "8.5") Double protein, + @Schema(description = "칼슘 (mg)", example = "200.0") Double calcium, + @Schema(description = "인 (mg)", example = "150.0") Double phosphorus, + @Schema(description = "나이아신 (mg)", example = "2.5") Double niacin, + @Schema(description = "비타민 B (mg)", example = "1.5") Double vitaminB, + @Schema(description = "비타민 E (mg)", example = "3.0") Double vitaminE ) { } \ No newline at end of file From 7b0bef9aa5d018aa654017bf98de1bf558a24991 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 19 Apr 2025 00:42:51 +0900 Subject: [PATCH 055/309] #18 feat: OpenAI Error Handling --- .../java/com/webeye/backend/global/error/ErrorCode.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/webeye/backend/global/error/ErrorCode.java b/src/main/java/com/webeye/backend/global/error/ErrorCode.java index 2630ae6..6f36027 100644 --- a/src/main/java/com/webeye/backend/global/error/ErrorCode.java +++ b/src/main/java/com/webeye/backend/global/error/ErrorCode.java @@ -7,6 +7,13 @@ @Getter @RequiredArgsConstructor public enum ErrorCode { + + // open ai + OPEN_AI_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "OpenAI의 응답을 받지 못했습니다."), + OPEN_AI_REFUSAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "OpenAI에서 형식에 맞지 않는 응답이 반환되었습니다."), + FILE_EXTENSION_NOT_FOUND(HttpStatus.BAD_REQUEST, "URL에서 확장자를 찾을 수 없습니다."), + UNSUPPORTED_IMAGE_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 형식입니다."), + INVALID_IMAGE_URL(HttpStatus.BAD_REQUEST, "잘못된 이미지 URL입니다.") ; private final HttpStatus status; private final String errorMessage; From fcc682c78ad649de4388dd1a4714afe64edecfed Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 19 Apr 2025 00:57:17 +0900 Subject: [PATCH 056/309] #18 feat: ErrorHandling --- .../backend/imageanalysis/infrastructure/ImageMimeType.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageMimeType.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageMimeType.java index 13186f3..1326bb4 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageMimeType.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageMimeType.java @@ -1,5 +1,6 @@ package com.webeye.backend.imageanalysis.infrastructure; +import com.webeye.backend.global.error.BusinessException; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -7,6 +8,8 @@ import java.util.Map; import java.util.stream.Collectors; +import static com.webeye.backend.global.error.ErrorCode.UNSUPPORTED_IMAGE_TYPE; + public enum ImageMimeType { PNG("png", MimeTypeUtils.IMAGE_PNG), JPEG("jpeg", MimeTypeUtils.IMAGE_JPEG), @@ -33,7 +36,7 @@ public static MimeType fromExtension(String extension) { String lowerExtension = extension.toLowerCase(); MimeType mimeType = EXTENSION_TO_MIMETYPE_MAP.get(lowerExtension); if (mimeType == null) { - throw new IllegalArgumentException("Unsupported image extension: " + extension); + throw new BusinessException(UNSUPPORTED_IMAGE_TYPE); } return mimeType; } From 8acdb997943ee4e74cbcdce744737150383eeb92 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 20 Apr 2025 03:43:47 +0900 Subject: [PATCH 057/309] =?UTF-8?q?#20=20feat:=20latest=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0c31852..051e86a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI/CD Pipeline on: push: - branches: [ "main", "develop" ] + branches: [ "main", "develop", "feat/#20-ncp" ] jobs: build-docker-image: @@ -47,7 +47,7 @@ jobs: # 6. Docker 이미지 빌드 - name: Build Docker Image - run: docker build -t ${{ secrets.DOCKER_USERNAME }}/webeye-server:${{ github.sha }} . + run: docker build -t ${{ secrets.DOCKER_USERNAME }}/webeye-server:latest . # 7. Docker Hub 로그인 - name: Log in to Docker Hub @@ -58,7 +58,7 @@ jobs: # 8. Docker 이미지 푸시 - name: Push Docker Image - run: docker push ${{ secrets.DOCKER_USERNAME }}/webeye-server:${{ github.sha }} + run: docker push ${{ secrets.DOCKER_USERNAME }}/webeye-server:latest deploy-pipeline: needs: build-docker-image @@ -77,7 +77,7 @@ jobs: username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_PRIVATE_KEY }} script: | - docker pull ${{ secrets.DOCKER_USERNAME }}/webeye-server:${{ github.sha }} + docker pull ${{ secrets.DOCKER_USERNAME }}/webeye-server:latest docker stop my-app || true docker rm my-app || true docker run -d -p 8080:8080 --name my-app \ @@ -85,4 +85,4 @@ jobs: -e SPRING_DATASOURCE_URL=jdbc:mysql://${{ secrets.RDS_ENDPOINT }}:3306/webeye \ -e SPRING_DATASOURCE_USERNAME=${{ secrets.RDS_USERNAME }} \ -e SPRING_DATASOURCE_PASSWORD=${{ secrets.RDS_PASSWORD }} \ - ${{ secrets.DOCKER_USERNAME }}/webeye-server:${{ github.sha }} \ No newline at end of file + ${{ secrets.DOCKER_USERNAME }}/webeye-server:latest \ No newline at end of file From fbe65846a7752b77038416d7b56055bd2ae65450 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 20 Apr 2025 17:52:07 +0900 Subject: [PATCH 058/309] #20 feat: ncp deploy test 1 --- .github/workflows/deploy.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 051e86a..e9fc48a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -69,20 +69,20 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - # 2. AWS EC2에 배포 - - name: Deploy to AWS EC2 + # 2. NCP 배포 + - name: Deploy to NCP uses: appleboy/ssh-action@v0.1.10 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_PRIVATE_KEY }} + host: ${{ secrets.NCP_HOST }} + username: ${{ secrets.NCP_USER }} + key: ${{ secrets.NCP_PRIVATE_KEY }} script: | docker pull ${{ secrets.DOCKER_USERNAME }}/webeye-server:latest docker stop my-app || true docker rm my-app || true docker run -d -p 8080:8080 --name my-app \ -e TZ=Asia/Seoul \ - -e SPRING_DATASOURCE_URL=jdbc:mysql://${{ secrets.RDS_ENDPOINT }}:3306/webeye \ - -e SPRING_DATASOURCE_USERNAME=${{ secrets.RDS_USERNAME }} \ - -e SPRING_DATASOURCE_PASSWORD=${{ secrets.RDS_PASSWORD }} \ + -e SPRING_DATASOURCE_URL=jdbc:mysql://${{ secrets.DB_ENDPOINT }}:3306/webeye \ + -e SPRING_DATASOURCE_USERNAME=${{ secrets.DB_USERNAME }} \ + -e SPRING_DATASOURCE_PASSWORD=${{ secrets.DB_PASSWORD }} \ ${{ secrets.DOCKER_USERNAME }}/webeye-server:latest \ No newline at end of file From 49034760c8fb813d2a4781ca1c7995d8a95248ff Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 20 Apr 2025 19:21:25 +0900 Subject: [PATCH 059/309] =?UTF-8?q?#20=20fix:=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e9fc48a..c8de8a4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI/CD Pipeline on: push: - branches: [ "main", "develop", "feat/#20-ncp" ] + branches: [ "main", "develop" ] jobs: build-docker-image: From fd4ae934dee00dbf57f1bb059cd37fee678c6b87 Mon Sep 17 00:00:00 2001 From: zyovn Date: Mon, 21 Apr 2025 03:36:24 +0900 Subject: [PATCH 060/309] =?UTF-8?q?#17=20chore:=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/RawMaterialService.java | 7 ------- .../application/scheduler/RawMaterialScheduler.java | 4 ++-- .../application/service/RawMaterialService.java | 7 +++++++ .../application/service/RawMaterialServiceImpl.java | 12 ++++++------ .../{domain => }/rawmaterial/domain/RawMaterial.java | 2 +- .../rawmaterial/dto/RawMaterialResponseDTO.java | 2 +- .../infrastructure/client/RawMaterialClient.java | 4 ++-- .../infrastructure/mapper/RawMaterialMapper.java | 6 +++--- .../persistence/RawMaterialRepository.java | 4 ++-- .../presentation/RawMaterialController.java | 8 ++++---- .../presentation/swagger/RawMaterialSwagger.java | 4 ++-- 11 files changed, 30 insertions(+), 30 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java rename src/main/java/com/webeye/backend/{domain => }/rawmaterial/application/scheduler/RawMaterialScheduler.java (72%) create mode 100644 src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java rename src/main/java/com/webeye/backend/{domain => }/rawmaterial/application/service/RawMaterialServiceImpl.java (80%) rename src/main/java/com/webeye/backend/{domain => }/rawmaterial/domain/RawMaterial.java (98%) rename src/main/java/com/webeye/backend/{domain => }/rawmaterial/dto/RawMaterialResponseDTO.java (96%) rename src/main/java/com/webeye/backend/{domain => }/rawmaterial/infrastructure/client/RawMaterialClient.java (84%) rename src/main/java/com/webeye/backend/{domain => }/rawmaterial/infrastructure/mapper/RawMaterialMapper.java (93%) rename src/main/java/com/webeye/backend/{domain => }/rawmaterial/infrastructure/persistence/RawMaterialRepository.java (51%) rename src/main/java/com/webeye/backend/{domain => }/rawmaterial/presentation/RawMaterialController.java (72%) rename src/main/java/com/webeye/backend/{domain => }/rawmaterial/presentation/swagger/RawMaterialSwagger.java (86%) diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java b/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java deleted file mode 100644 index 1407a3a..0000000 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.webeye.backend.domain.rawmaterial.application.service; - -import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; - -public interface RawMaterialService { - RawMaterialResponseDTO.Body callRawMaterialAPI(); -} diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/application/scheduler/RawMaterialScheduler.java b/src/main/java/com/webeye/backend/rawmaterial/application/scheduler/RawMaterialScheduler.java similarity index 72% rename from src/main/java/com/webeye/backend/domain/rawmaterial/application/scheduler/RawMaterialScheduler.java rename to src/main/java/com/webeye/backend/rawmaterial/application/scheduler/RawMaterialScheduler.java index ab5e6d9..8b840cd 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/application/scheduler/RawMaterialScheduler.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/scheduler/RawMaterialScheduler.java @@ -1,6 +1,6 @@ -package com.webeye.backend.domain.rawmaterial.application.scheduler; +package com.webeye.backend.rawmaterial.application.scheduler; -import com.webeye.backend.domain.rawmaterial.application.service.RawMaterialService; +import com.webeye.backend.rawmaterial.application.service.RawMaterialService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java new file mode 100644 index 0000000..147a7be --- /dev/null +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java @@ -0,0 +1,7 @@ +package com.webeye.backend.rawmaterial.application.service; + +import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; + +public interface RawMaterialService { + RawMaterialResponseDTO.Body callRawMaterialAPI(); +} diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java similarity index 80% rename from src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java rename to src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java index 6bec43b..14fc048 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/application/service/RawMaterialServiceImpl.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java @@ -1,10 +1,10 @@ -package com.webeye.backend.domain.rawmaterial.application.service; +package com.webeye.backend.rawmaterial.application.service; -import com.webeye.backend.domain.rawmaterial.infrastructure.mapper.RawMaterialMapper; -import com.webeye.backend.domain.rawmaterial.domain.RawMaterial; -import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; -import com.webeye.backend.domain.rawmaterial.infrastructure.client.RawMaterialClient; -import com.webeye.backend.domain.rawmaterial.infrastructure.persistence.RawMaterialRepository; +import com.webeye.backend.rawmaterial.infrastructure.mapper.RawMaterialMapper; +import com.webeye.backend.rawmaterial.domain.RawMaterial; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.infrastructure.client.RawMaterialClient; +import com.webeye.backend.rawmaterial.infrastructure.persistence.RawMaterialRepository; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/domain/RawMaterial.java b/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java similarity index 98% rename from src/main/java/com/webeye/backend/domain/rawmaterial/domain/RawMaterial.java rename to src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java index 31ddd6f..5c291b1 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/domain/RawMaterial.java +++ b/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java @@ -1,4 +1,4 @@ -package com.webeye.backend.domain.rawmaterial.domain; +package com.webeye.backend.rawmaterial.domain; import com.webeye.backend.global.domain.BaseEntity; import jakarta.persistence.*; diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/dto/RawMaterialResponseDTO.java b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponseDTO.java similarity index 96% rename from src/main/java/com/webeye/backend/domain/rawmaterial/dto/RawMaterialResponseDTO.java rename to src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponseDTO.java index 0743dd0..51ab0df 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/dto/RawMaterialResponseDTO.java +++ b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponseDTO.java @@ -1,4 +1,4 @@ -package com.webeye.backend.domain.rawmaterial.dto; +package com.webeye.backend.rawmaterial.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/client/RawMaterialClient.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java similarity index 84% rename from src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/client/RawMaterialClient.java rename to src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java index 79630fe..cf02dce 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/client/RawMaterialClient.java +++ b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java @@ -1,6 +1,6 @@ -package com.webeye.backend.domain.rawmaterial.infrastructure.client; +package com.webeye.backend.rawmaterial.infrastructure.client; -import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; import com.webeye.backend.global.config.OpenFeignConfig; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/mapper/RawMaterialMapper.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java similarity index 93% rename from src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/mapper/RawMaterialMapper.java rename to src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java index c40e6c9..9ad6636 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/mapper/RawMaterialMapper.java +++ b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java @@ -1,7 +1,7 @@ -package com.webeye.backend.domain.rawmaterial.infrastructure.mapper; +package com.webeye.backend.rawmaterial.infrastructure.mapper; -import com.webeye.backend.domain.rawmaterial.domain.RawMaterial; -import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.domain.RawMaterial; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; import java.util.List; import java.util.stream.Collectors; diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/persistence/RawMaterialRepository.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/persistence/RawMaterialRepository.java similarity index 51% rename from src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/persistence/RawMaterialRepository.java rename to src/main/java/com/webeye/backend/rawmaterial/infrastructure/persistence/RawMaterialRepository.java index cb17bea..93f7eb4 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/infrastructure/persistence/RawMaterialRepository.java +++ b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/persistence/RawMaterialRepository.java @@ -1,6 +1,6 @@ -package com.webeye.backend.domain.rawmaterial.infrastructure.persistence; +package com.webeye.backend.rawmaterial.infrastructure.persistence; -import com.webeye.backend.domain.rawmaterial.domain.RawMaterial; +import com.webeye.backend.rawmaterial.domain.RawMaterial; import org.springframework.data.jpa.repository.JpaRepository; public interface RawMaterialRepository extends JpaRepository { diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java b/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java similarity index 72% rename from src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java rename to src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java index 3ffa5d9..420e9bc 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/RawMaterialController.java +++ b/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java @@ -1,8 +1,8 @@ -package com.webeye.backend.domain.rawmaterial.presentation; +package com.webeye.backend.rawmaterial.presentation; -import com.webeye.backend.domain.rawmaterial.application.service.RawMaterialService; -import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; -import com.webeye.backend.domain.rawmaterial.presentation.swagger.RawMaterialSwagger; +import com.webeye.backend.rawmaterial.application.service.RawMaterialService; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.presentation.swagger.RawMaterialSwagger; import com.webeye.backend.global.dto.response.SuccessResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/swagger/RawMaterialSwagger.java b/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java similarity index 86% rename from src/main/java/com/webeye/backend/domain/rawmaterial/presentation/swagger/RawMaterialSwagger.java rename to src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java index b09ae72..33ce504 100644 --- a/src/main/java/com/webeye/backend/domain/rawmaterial/presentation/swagger/RawMaterialSwagger.java +++ b/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java @@ -1,6 +1,6 @@ -package com.webeye.backend.domain.rawmaterial.presentation.swagger; +package com.webeye.backend.rawmaterial.presentation.swagger; -import com.webeye.backend.domain.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; import com.webeye.backend.global.dto.response.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; From 69dfbf5c448512f7def23446ac290bda466c82be Mon Sep 17 00:00:00 2001 From: zyovn Date: Mon, 21 Apr 2025 17:58:29 +0900 Subject: [PATCH 061/309] =?UTF-8?q?#17=20refactpr:=20dto=20record=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RawMaterialService.java | 4 +- .../service/RawMaterialServiceImpl.java | 22 ++- .../rawmaterial/dto/RawMaterialResponse.java | 50 ++++++ .../dto/RawMaterialResponseDTO.java | 71 --------- .../client/RawMaterialClient.java | 4 +- .../mapper/RawMaterialMapper.java | 143 ++++++++++-------- .../presentation/RawMaterialController.java | 6 +- .../swagger/RawMaterialSwagger.java | 4 +- 8 files changed, 146 insertions(+), 158 deletions(-) create mode 100644 src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponseDTO.java diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java index 147a7be..ed50725 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java @@ -1,7 +1,7 @@ package com.webeye.backend.rawmaterial.application.service; -import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; public interface RawMaterialService { - RawMaterialResponseDTO.Body callRawMaterialAPI(); + RawMaterialResponse.Body callRawMaterialAPI(int pageNo, int numOfRows); } diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java index 14fc048..aad7943 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java @@ -2,7 +2,7 @@ import com.webeye.backend.rawmaterial.infrastructure.mapper.RawMaterialMapper; import com.webeye.backend.rawmaterial.domain.RawMaterial; -import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; import com.webeye.backend.rawmaterial.infrastructure.client.RawMaterialClient; import com.webeye.backend.rawmaterial.infrastructure.persistence.RawMaterialRepository; import com.webeye.backend.global.error.BusinessException; @@ -29,32 +29,30 @@ public class RawMaterialServiceImpl implements RawMaterialService { @Override @Transactional - public RawMaterialResponseDTO.Body callRawMaterialAPI() { - RawMaterialResponseDTO response = rawMaterialClient.getRawMaterialInfo(serviceKey, 1, 100, "json"); + public RawMaterialResponse.Body callRawMaterialAPI(int pageNo, int numOfRows) { + RawMaterialResponse response = rawMaterialClient.getRawMaterialInfo(serviceKey, pageNo, numOfRows, "json"); - RawMaterialResponseDTO.Response responseDto = response.getResponse(); + RawMaterialResponse.Response responseDto = response.response(); - List items = responseDto.getBody().getItems(); + List items = responseDto.body().items(); validateRawMaterial(responseDto, items); - List rawMaterials = items.stream() - .map(RawMaterialMapper::toEntity) - .collect(Collectors.toList()); + List rawMaterials = RawMaterialMapper.toEntityList(items); List saved = rawMaterialRepository.saveAll(rawMaterials); - return RawMaterialMapper.ofList(saved); + return RawMaterialMapper.of(saved, pageNo, numOfRows, items.size()); } - private void validateRawMaterial(RawMaterialResponseDTO.Response response, List items) { + private void validateRawMaterial(RawMaterialResponse.Response response, List items) { // Open API 응답 실패 - if (response == null || response.getBody() == null) { + if (response == null || response.body() == null) { throw new BusinessException(ErrorCode.OPEN_API_RESPONSE_NULL); } // items not found - if (items == null || response.getBody().getItems().isEmpty()) { + if (items == null || response.body().items().isEmpty()) { throw new BusinessException(ErrorCode.OPEN_API_DATA_MISSING); } } diff --git a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java new file mode 100644 index 0000000..621b174 --- /dev/null +++ b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java @@ -0,0 +1,50 @@ +package com.webeye.backend.rawmaterial.dto; + +import java.util.List; + +public record RawMaterialResponse( + Response response +) { + public record Response( + Body body + ) {} + + public record Body( + List items, + String totalCount, + String numOfRows, + String pageNo + ) {} + + public record Item( + Long rawMaterialId, + String foodCd, + String foodNm, + String nutConSrtrQua, + double enerc, + double water, + double prot, + double fatce, + double ash, + double chocdf, + double sugar, + double fibtg, + double ca, + double fe, + double p, + double k, + double nat, + double vitaRae, + double retol, + double cartb, + double thia, + double ribf, + double nia, + double vitc, + double vitd, + double chole, + double fasat, + double fartn + ) {} +} + diff --git a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponseDTO.java b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponseDTO.java deleted file mode 100644 index 51ab0df..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponseDTO.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.webeye.backend.rawmaterial.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class RawMaterialResponseDTO { - - private Response response; - - @Getter - @Builder - @AllArgsConstructor - @NoArgsConstructor - public static class Response { - private Body body; - } - - @Getter - @Builder - @AllArgsConstructor - @NoArgsConstructor - public static class Body { - private List items; - private String totalCount; - private String numOfRows; - private String pageNo; - } - - @Getter - @Builder - @AllArgsConstructor - @NoArgsConstructor - public static class Item { - private Long rawMaterialId; - private String foodCd; - private String foodNm; - private String nutConSrtrQua; - private double enerc; - private double water; - private double prot; - private double fatce; - private double ash; - private double chocdf; - private double sugar; - private double fibtg; - private double ca; - private double fe; - private double p; - private double k; - private double nat; - private double vitaRae; - private double retol; - private double cartb; - private double thia; - private double ribf; - private double nia; - private double vitc; - private double vitd; - private double chole; - private double fasat; - private double fartn; - } -} diff --git a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java index cf02dce..1875f54 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java +++ b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java @@ -1,6 +1,6 @@ package com.webeye.backend.rawmaterial.infrastructure.client; -import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; import com.webeye.backend.global.config.OpenFeignConfig; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @@ -17,7 +17,7 @@ public interface RawMaterialClient { value = "/openapi/tn_pubr_public_nutri_material_info_api", produces = "application/json" ) - RawMaterialResponseDTO getRawMaterialInfo( + RawMaterialResponse getRawMaterialInfo( @RequestParam("serviceKey") String serviceKey, @RequestParam("pageNo") int pageNo, @RequestParam("numOfRows") int numOfRows, diff --git a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java index 9ad6636..7cfc5d7 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java +++ b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java @@ -1,84 +1,95 @@ package com.webeye.backend.rawmaterial.infrastructure.mapper; import com.webeye.backend.rawmaterial.domain.RawMaterial; -import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; +import org.springframework.data.domain.Page; import java.util.List; import java.util.stream.Collectors; public class RawMaterialMapper { - public static RawMaterial toEntity(RawMaterialResponseDTO.Item item) { + public static RawMaterial toEntity(RawMaterialResponse.Item item) { return RawMaterial.builder() - .foodCd(item.getFoodCd()) - .foodNm(item.getFoodNm()) - .nutConSrtrQua(item.getNutConSrtrQua()) - .enerc(item.getEnerc()) - .water(item.getWater()) - .prot(item.getProt()) - .fatce(item.getFatce()) - .ash(item.getAsh()) - .chocdf(item.getChocdf()) - .sugar(item.getSugar()) - .fibtg(item.getFibtg()) - .ca(item.getCa()) - .fe(item.getFe()) - .p(item.getP()) - .k(item.getK()) - .nat(item.getNat()) - .vitaRae(item.getVitaRae()) - .retol(item.getRetol()) - .cartb(item.getCartb()) - .thia(item.getThia()) - .ribf(item.getRibf()) - .nia(item.getNia()) - .vitc(item.getVitc()) - .vitd(item.getVitd()) - .chole(item.getChole()) - .fasat(item.getFasat()) - .fartn(item.getFartn()) + .foodCd(item.foodCd()) + .foodNm(item.foodNm()) + .nutConSrtrQua(item.nutConSrtrQua()) + .enerc(item.enerc()) + .water(item.water()) + .prot(item.prot()) + .fatce(item.fatce()) + .ash(item.ash()) + .chocdf(item.chocdf()) + .sugar(item.sugar()) + .fibtg(item.fibtg()) + .ca(item.ca()) + .fe(item.fe()) + .p(item.p()) + .k(item.k()) + .nat(item.nat()) + .vitaRae(item.vitaRae()) + .retol(item.retol()) + .cartb(item.cartb()) + .thia(item.thia()) + .ribf(item.ribf()) + .nia(item.nia()) + .vitc(item.vitc()) + .vitd(item.vitd()) + .chole(item.chole()) + .fasat(item.fasat()) + .fartn(item.fartn()) .build(); } - public static RawMaterialResponseDTO.Item of(RawMaterial rawMaterial) { - return RawMaterialResponseDTO.Item.builder() - .rawMaterialId(rawMaterial.getId()) - .foodCd(rawMaterial.getFoodCd()) - .foodNm(rawMaterial.getFoodNm()) - .nutConSrtrQua(rawMaterial.getNutConSrtrQua()) - .enerc(rawMaterial.getEnerc()) - .water(rawMaterial.getWater()) - .prot(rawMaterial.getProt()) - .fatce(rawMaterial.getFatce()) - .ash(rawMaterial.getAsh()) - .chocdf(rawMaterial.getChocdf()) - .sugar(rawMaterial.getSugar()) - .fibtg(rawMaterial.getFibtg()) - .ca(rawMaterial.getCa()) - .fe(rawMaterial.getFe()) - .p(rawMaterial.getP()) - .k(rawMaterial.getK()) - .nat(rawMaterial.getNat()) - .vitaRae(rawMaterial.getVitaRae()) - .retol(rawMaterial.getRetol()) - .cartb(rawMaterial.getCartb()) - .thia(rawMaterial.getThia()) - .ribf(rawMaterial.getRibf()) - .nia(rawMaterial.getNia()) - .vitc(rawMaterial.getVitc()) - .vitd(rawMaterial.getVitd()) - .chole(rawMaterial.getChole()) - .fasat(rawMaterial.getFasat()) - .fartn(rawMaterial.getFartn()) - .build(); + public static RawMaterialResponse.Item of(RawMaterial rawMaterial) { + return new RawMaterialResponse.Item( + rawMaterial.getId(), + rawMaterial.getFoodCd(), + rawMaterial.getFoodNm(), + rawMaterial.getNutConSrtrQua(), + rawMaterial.getEnerc(), + rawMaterial.getWater(), + rawMaterial.getProt(), + rawMaterial.getFatce(), + rawMaterial.getAsh(), + rawMaterial.getChocdf(), + rawMaterial.getSugar(), + rawMaterial.getFibtg(), + rawMaterial.getCa(), + rawMaterial.getFe(), + rawMaterial.getP(), + rawMaterial.getK(), + rawMaterial.getNat(), + rawMaterial.getVitaRae(), + rawMaterial.getRetol(), + rawMaterial.getCartb(), + rawMaterial.getThia(), + rawMaterial.getRibf(), + rawMaterial.getNia(), + rawMaterial.getVitc(), + rawMaterial.getVitd(), + rawMaterial.getChole(), + rawMaterial.getFasat(), + rawMaterial.getFartn() + ); + } + + public static List toEntityList(List items) { + return items.stream() + .map(RawMaterialMapper::toEntity) + .collect(Collectors.toList()); } - public static RawMaterialResponseDTO.Body ofList(List rawMaterials) { - List rawMaterialResponseDTOList = rawMaterials.stream() - .map(RawMaterialMapper::of).collect(Collectors.toList()); + public static RawMaterialResponse.Body of(List list, int totalCount, int numOfRows, int pageNo) { + List itemList = list.stream() + .map(RawMaterialMapper::of) + .collect(Collectors.toList()); - return RawMaterialResponseDTO.Body.builder() - .items(rawMaterialResponseDTOList) - .build(); + return new RawMaterialResponse.Body( + itemList, + String.valueOf(totalCount), + String.valueOf(numOfRows), + String.valueOf(pageNo) + ); } } diff --git a/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java b/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java index 420e9bc..9a0c14a 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java +++ b/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java @@ -1,7 +1,7 @@ package com.webeye.backend.rawmaterial.presentation; import com.webeye.backend.rawmaterial.application.service.RawMaterialService; -import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; import com.webeye.backend.rawmaterial.presentation.swagger.RawMaterialSwagger; import com.webeye.backend.global.dto.response.SuccessResponse; import lombok.RequiredArgsConstructor; @@ -19,7 +19,7 @@ public class RawMaterialController implements RawMaterialSwagger { private final RawMaterialService rawMaterialService; @GetMapping - public SuccessResponse callRawMaterialApi() { - return SuccessResponse.of(RAW_MATERIAL_API_SUCCESS, rawMaterialService.callRawMaterialAPI()); + public SuccessResponse callRawMaterialApi(int pageNo, int numOfRows) { + return SuccessResponse.of(RAW_MATERIAL_API_SUCCESS, rawMaterialService.callRawMaterialAPI(pageNo, numOfRows)); } } diff --git a/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java b/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java index 33ce504..1dd0f39 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java +++ b/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java @@ -1,6 +1,6 @@ package com.webeye.backend.rawmaterial.presentation.swagger; -import com.webeye.backend.rawmaterial.dto.RawMaterialResponseDTO; +import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; import com.webeye.backend.global.dto.response.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -19,5 +19,5 @@ public interface RawMaterialSwagger { description = "OPEN API가 성공적으로 호출되었습니다." ) }) - SuccessResponse callRawMaterialApi(); + SuccessResponse callRawMaterialApi(int pageNo, int numOfRows); } From 62f0c3ffa742db3393b4476904591539e4d0c816 Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 23 Apr 2025 15:51:19 +0900 Subject: [PATCH 062/309] =?UTF-8?q?#23=20feat:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EC=B4=88=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/dto/response/type/SuccessCode.java | 2 ++ .../infrastructure/OpenAiClient.java | 1 - .../review/application/ReviewService.java | 17 +++++++++++ .../review/dto/response/ReviewResponse.java | 16 ++++++++++ .../review/presentation/ReviewController.java | 29 +++++++++++++++++++ .../presentation/swagger/ReviewSwagger.java | 23 +++++++++++++++ 6 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/webeye/backend/review/application/ReviewService.java create mode 100644 src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java create mode 100644 src/main/java/com/webeye/backend/review/presentation/ReviewController.java create mode 100644 src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index 7fd8bf1..4788ca4 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -20,6 +20,8 @@ public enum SuccessCode { PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS(200, "Product point explanation analysis success"), PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS(200, "Product detail explanation analysis success"), + // review + REVIEW_SUMMARY_SUCCESS(200, "Review summary success"), ; private final int status; diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index a6cc788..91deb9e 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -106,7 +106,6 @@ public NutritionResponse explainNutrition(ImageAnalysisRequest request) { return callWithStructuredOutput(request, prompt, NutritionResponse.class); } - private T callWithStructuredOutput(ImageAnalysisRequest request, ImageAnalysisPrompt prompt, Class clazz) { BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java new file mode 100644 index 0000000..5dc6714 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -0,0 +1,17 @@ +package com.webeye.backend.review.application; + +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.review.dto.response.ReviewResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final OpenAiClient openAiClient; + + public ReviewResponse summarizeReview() { + return null; + } +} diff --git a/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java b/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java new file mode 100644 index 0000000..bd03221 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java @@ -0,0 +1,16 @@ +package com.webeye.backend.review.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "리뷰 요약") +public record ReviewResponse( + @Schema(description = "별점", example = "4.5") + Double score, + @Schema(description = "긍정 리뷰", example = "맛있다는 평가가 많습니다.") + String positive, + @Schema(description = "부정 리뷰", example = "배송이 느리다는 평가가 많습니다.") + String negative, + @Schema(description = "키워드", example = "맛있어요, 신선해요") + String keyword +) { +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/review/presentation/ReviewController.java b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java new file mode 100644 index 0000000..4ce7288 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java @@ -0,0 +1,29 @@ +package com.webeye.backend.review.presentation; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.review.application.ReviewService; +import com.webeye.backend.review.dto.response.ReviewResponse; +import com.webeye.backend.review.presentation.swagger.ReviewSwagger; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.REVIEW_SUMMARY_SUCCESS; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/review/summary") +public class ReviewController implements ReviewSwagger { + + private final ReviewService reviewService; + + @Override + @PostMapping + @ResponseStatus(HttpStatus.OK) + public SuccessResponse summarizeReview() { + return SuccessResponse.of(REVIEW_SUMMARY_SUCCESS, reviewService.summarizeReview()); + } +} diff --git a/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java b/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java new file mode 100644 index 0000000..0634c2b --- /dev/null +++ b/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java @@ -0,0 +1,23 @@ +package com.webeye.backend.review.presentation.swagger; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.review.dto.response.ReviewResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "[리뷰 요약]", description = "리뷰 요약 및 분석 관련 API") +public interface ReviewSwagger { + @Operation( + summary = "제품의 리뷰 요약 및 분석", + description = "해당 제품에 대한 전체적인 리뷰를 요약 및 분석합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "리뷰 요약 및 분석이 성공적으로 수행되었습니다." + ) + }) + SuccessResponse summarizeReview(); +} From 0c1c378be975427a869e779be238b079fb3923ff Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 23 Apr 2025 22:46:57 +0900 Subject: [PATCH 063/309] =?UTF-8?q?#23=20feat:=20clovax=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EA=B5=AC=EC=A1=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/dto/response/ReviewResponse.java | 4 +++- .../infrastructure/clovaX/ClovaXClient.java | 4 ++++ .../infrastructure/clovaX/ClovaXService.java | 4 ++++ .../clovaX/domain/ContentType.java | 15 +++++++++++++++ .../infrastructure/clovaX/domain/Role.java | 16 ++++++++++++++++ .../clovaX/dto/request/ClovaXContent.java | 9 +++++++++ .../clovaX/dto/request/ClovaXMessage.java | 11 +++++++++++ .../clovaX/dto/request/ClovaXRequest.java | 8 ++++++++ 8 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXService.java create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXRequest.java diff --git a/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java b/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java index bd03221..4834034 100644 --- a/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java +++ b/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java @@ -2,6 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + @Schema(description = "리뷰 요약") public record ReviewResponse( @Schema(description = "별점", example = "4.5") @@ -11,6 +13,6 @@ public record ReviewResponse( @Schema(description = "부정 리뷰", example = "배송이 느리다는 평가가 많습니다.") String negative, @Schema(description = "키워드", example = "맛있어요, 신선해요") - String keyword + List keywords ) { } \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java new file mode 100644 index 0000000..e8ed35f --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java @@ -0,0 +1,4 @@ +package com.webeye.backend.review.infrastructure.clovaX; + +public class ClovaXClient { +} diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXService.java new file mode 100644 index 0000000..398c07e --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXService.java @@ -0,0 +1,4 @@ +package com.webeye.backend.review.infrastructure.clovaX; + +public class ClovaXService { +} diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java new file mode 100644 index 0000000..d418715 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java @@ -0,0 +1,15 @@ +package com.webeye.backend.review.infrastructure.clovaX.domain; + +import lombok.Getter; + +@Getter +public enum ContentType { + TEXT("text"), + IMAGE_URL("image_url"); + + private final String value; + + ContentType(String value) { + this.value = value; + } +} diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java new file mode 100644 index 0000000..d72d332 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java @@ -0,0 +1,16 @@ +package com.webeye.backend.review.infrastructure.clovaX.domain; + +import lombok.Getter; + +@Getter +public enum Role { + SYSTEM("system"), + USER("user"), + ASSISTANT("assistant"); + + private final String value; + + Role(String role) { + this.value = role; + } +} diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java new file mode 100644 index 0000000..42a6f98 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java @@ -0,0 +1,9 @@ +package com.webeye.backend.review.infrastructure.clovaX.dto.request; + +import com.webeye.backend.review.infrastructure.clovaX.domain.ContentType; + +public record ClovaXContent( + ContentType contentType, + String text +) { +} diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java new file mode 100644 index 0000000..7ffbabd --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java @@ -0,0 +1,11 @@ +package com.webeye.backend.review.infrastructure.clovaX.dto.request; + +import com.webeye.backend.review.infrastructure.clovaX.domain.Role; + +import java.util.List; + +public record ClovaXMessage( + Role role, + List contents +) { +} diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXRequest.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXRequest.java new file mode 100644 index 0000000..692c4e7 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXRequest.java @@ -0,0 +1,8 @@ +package com.webeye.backend.review.infrastructure.clovaX.dto.request; + +import java.util.List; + +public record ClovaXRequest( + List messages +) { +} From 3723fc9014c5f0e9e3eb6b778dda927834b73ae8 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 24 Apr 2025 17:11:55 +0900 Subject: [PATCH 064/309] =?UTF-8?q?#17=20feat:=20requestParam=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/RawMaterialScheduler.java | 20 ------------------- .../service/RawMaterialService.java | 2 +- .../service/RawMaterialServiceImpl.java | 3 +-- .../rawmaterial/dto/RawMaterialResponse.java | 3 +-- .../presentation/RawMaterialController.java | 17 +++++++++++----- .../swagger/RawMaterialSwagger.java | 6 +++++- 6 files changed, 20 insertions(+), 31 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/application/scheduler/RawMaterialScheduler.java diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/scheduler/RawMaterialScheduler.java b/src/main/java/com/webeye/backend/rawmaterial/application/scheduler/RawMaterialScheduler.java deleted file mode 100644 index 8b840cd..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/application/scheduler/RawMaterialScheduler.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.webeye.backend.rawmaterial.application.scheduler; - -import com.webeye.backend.rawmaterial.application.service.RawMaterialService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -public class RawMaterialScheduler { - - private final RawMaterialService rawMaterialService; - - @Scheduled(cron = "0 0 0 1 3 *") - public void updateRawMaterial() { - - } -} diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java index ed50725..1ded6a0 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java @@ -3,5 +3,5 @@ import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; public interface RawMaterialService { - RawMaterialResponse.Body callRawMaterialAPI(int pageNo, int numOfRows); + RawMaterialResponse.Body callRawMaterialAPI(Integer pageNo, Integer numOfRows); } diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java index aad7943..28bca7c 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java @@ -14,7 +14,6 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.stream.Collectors; @Slf4j @Service @@ -29,7 +28,7 @@ public class RawMaterialServiceImpl implements RawMaterialService { @Override @Transactional - public RawMaterialResponse.Body callRawMaterialAPI(int pageNo, int numOfRows) { + public RawMaterialResponse.Body callRawMaterialAPI(Integer pageNo, Integer numOfRows) { RawMaterialResponse response = rawMaterialClient.getRawMaterialInfo(serviceKey, pageNo, numOfRows, "json"); RawMaterialResponse.Response responseDto = response.response(); diff --git a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java index 621b174..f140af9 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java +++ b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java @@ -46,5 +46,4 @@ public record Item( double fasat, double fartn ) {} -} - +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java b/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java index 9a0c14a..95e9978 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java +++ b/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java @@ -5,9 +5,8 @@ import com.webeye.backend.rawmaterial.presentation.swagger.RawMaterialSwagger; import com.webeye.backend.global.dto.response.SuccessResponse; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; import static com.webeye.backend.global.dto.response.type.SuccessCode.RAW_MATERIAL_API_SUCCESS; @@ -18,8 +17,16 @@ public class RawMaterialController implements RawMaterialSwagger { private final RawMaterialService rawMaterialService; + @Override + @ResponseStatus(HttpStatus.OK) @GetMapping - public SuccessResponse callRawMaterialApi(int pageNo, int numOfRows) { - return SuccessResponse.of(RAW_MATERIAL_API_SUCCESS, rawMaterialService.callRawMaterialAPI(pageNo, numOfRows)); + public SuccessResponse callRawMaterialApi( + @RequestParam Integer pageNo, + @RequestParam Integer numOfRows + ) { + return SuccessResponse.of( + RAW_MATERIAL_API_SUCCESS, + rawMaterialService.callRawMaterialAPI(pageNo, numOfRows) + ); } } diff --git a/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java b/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java index 1dd0f39..9c27300 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java +++ b/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RequestParam; @Tag(name = "[원재료 주의 영양 성분]", description = "원재료 주의 영양 성분 표시 관련 API") public interface RawMaterialSwagger { @@ -19,5 +20,8 @@ public interface RawMaterialSwagger { description = "OPEN API가 성공적으로 호출되었습니다." ) }) - SuccessResponse callRawMaterialApi(int pageNo, int numOfRows); + SuccessResponse callRawMaterialApi( + @RequestParam Integer pageNo, + @RequestParam Integer numOfRows + ); } From 7eb771c028ffb8d0f34cae220795364bcdf26f53 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 25 Apr 2025 01:52:15 +0900 Subject: [PATCH 065/309] =?UTF-8?q?#17=20refactor:=20dto=20schema=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RawMaterialServiceImpl.java | 4 +- .../rawmaterial/dto/RawMaterialResponse.java | 40 ++++++++++++++++++- .../mapper/RawMaterialMapper.java | 7 ++-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java index 28bca7c..d43cb7f 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java @@ -39,9 +39,9 @@ public RawMaterialResponse.Body callRawMaterialAPI(Integer pageNo, Integer numOf List rawMaterials = RawMaterialMapper.toEntityList(items); - List saved = rawMaterialRepository.saveAll(rawMaterials); + // List savedRawMaterials = rawMaterialRepository.saveAll(rawMaterials); - return RawMaterialMapper.of(saved, pageNo, numOfRows, items.size()); + return RawMaterialMapper.of(rawMaterials, pageNo, numOfRows, items.size()); } private void validateRawMaterial(RawMaterialResponse.Response response, List items) { diff --git a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java index f140af9..9f7921d 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java +++ b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java @@ -1,49 +1,85 @@ package com.webeye.backend.rawmaterial.dto; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; +@Schema(description = "원재료 영양 성분") public record RawMaterialResponse( Response response ) { + public record Response( Body body ) {} public record Body( + @Schema(description = "원재료 목록") List items, - String totalCount, + @Schema(description = "페이지 번호", example = "1") + String pageNo, + @Schema(description = "페이지 내 데이터 개수", example = "10") String numOfRows, - String pageNo + @Schema(description = "총 데이터 개수", example = "3500") + String totalCount ) {} public record Item( + @Schema(description = "원재료 영양성분 ID", example = "1") Long rawMaterialId, + @Schema(description = "식품 코드", example = "R211-009111601-0001") String foodCd, + @Schema(description = "식품명", example = "청어_암컷,육_생것_포항_8월") String foodNm, + @Schema(description = "영양성분함량기준량", example = "100g") String nutConSrtrQua, + @Schema(description = "에너지", example = "90") double enerc, + @Schema(description = "수분", example = "40.7") double water, + @Schema(description = "단백질", example = "18.21") double prot, + @Schema(description = "지방", example = "3.14") double fatce, + @Schema(description = "회분", example = "1.2") double ash, + @Schema(description = "탄수화물", example = "0.61") double chocdf, + @Schema(description = "당류", example = "2.64") double sugar, + @Schema(description = "식이섬유", example = "21.7") double fibtg, + @Schema(description = "칼슘", example = "156") double ca, + @Schema(description = "철", example = "10.4") double fe, + @Schema(description = "인", example = "244") double p, + @Schema(description = "칼륨", example = "250") double k, + @Schema(description = "나트륨", example = "1300") double nat, + @Schema(description = "비타민 A", example = "1007") double vitaRae, + @Schema(description = "레티놀", example = "12") double retol, + @Schema(description = "베타카르틴", example = "2698") double cartb, + @Schema(description = "티아민", example = "0.968") double thia, + @Schema(description = "리보플라빈", example = "0.11") double ribf, + @Schema(description = "니아신", example = "6.3") double nia, + @Schema(description = "비타민 C", example = "1.75") double vitc, + @Schema(description = "비타민 D", example = "48") double vitd, + @Schema(description = "콜레스테롤", example = "22.57") double chole, + @Schema(description = "포화지방산", example = "0.83") double fasat, + @Schema(description = "트랜스지방산", example = "0") double fartn ) {} } \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java index 7cfc5d7..a2f9bf0 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java +++ b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java @@ -2,7 +2,6 @@ import com.webeye.backend.rawmaterial.domain.RawMaterial; import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; -import org.springframework.data.domain.Page; import java.util.List; import java.util.stream.Collectors; @@ -80,16 +79,16 @@ public static List toEntityList(List item .collect(Collectors.toList()); } - public static RawMaterialResponse.Body of(List list, int totalCount, int numOfRows, int pageNo) { + public static RawMaterialResponse.Body of(List list, int pageNo,int numOfRows, int totalCount) { List itemList = list.stream() .map(RawMaterialMapper::of) .collect(Collectors.toList()); return new RawMaterialResponse.Body( itemList, - String.valueOf(totalCount), + String.valueOf(pageNo), String.valueOf(numOfRows), - String.valueOf(pageNo) + String.valueOf(totalCount) ); } } From 373a741a879471f418caa59e255109cebd87162f Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 25 Apr 2025 02:04:05 +0900 Subject: [PATCH 066/309] =?UTF-8?q?#17=20refactor:=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RawMaterialServiceImpl.java | 2 +- .../rawmaterial/domain/RawMaterial.java | 54 +++++++++---------- .../rawmaterial/dto/RawMaterialResponse.java | 48 ++++++++--------- .../mapper/RawMaterialMapper.java | 2 +- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java index d43cb7f..7c7aefa 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java @@ -41,7 +41,7 @@ public RawMaterialResponse.Body callRawMaterialAPI(Integer pageNo, Integer numOf // List savedRawMaterials = rawMaterialRepository.saveAll(rawMaterials); - return RawMaterialMapper.of(rawMaterials, pageNo, numOfRows, items.size()); + return RawMaterialMapper.ofList(rawMaterials, pageNo, numOfRows, items.size()); } private void validateRawMaterial(RawMaterialResponse.Response response, List items) { diff --git a/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java b/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java index 5c291b1..0c61570 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java +++ b/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java @@ -27,81 +27,81 @@ public class RawMaterial extends BaseEntity { private String nutConSrtrQua; @Column - private double enerc; + private Double enerc; @Column - private double water; + private Double water; @Column - private double prot; + private Double prot; @Column - private double fatce; + private Double fatce; @Column - private double ash; + private Double ash; @Column - private double chocdf; + private Double chocdf; @Column - private double sugar; + private Double sugar; @Column - private double fibtg; + private Double fibtg; @Column - private double ca; + private Double ca; @Column - private double fe; + private Double fe; @Column - private double p; + private Double p; @Column - private double k; + private Double k; @Column - private double nat; + private Double nat; @Column - private double vitaRae; + private Double vitaRae; @Column - private double retol; + private Double retol; @Column - private double cartb; + private Double cartb; @Column - private double thia; + private Double thia; @Column - private double ribf; + private Double ribf; @Column - private double nia; + private Double nia; @Column - private double vitc; + private Double vitc; @Column - private double vitd; + private Double vitd; @Column - private double chole; + private Double chole; @Column - private double fasat; + private Double fasat; @Column - private double fartn; + private Double fartn; @Builder - private RawMaterial(Long id, String foodCd, String foodNm, String nutConSrtrQua, double enerc, double water, double prot, double fatce, double ash, - double chocdf, double sugar, double fibtg, double ca, double fe, double p, double k, double nat, double vitaRae, double retol, - double cartb, double thia, double ribf, double nia, double vitc, double vitd, double chole, double fasat, double fartn) { + private RawMaterial(Long id, String foodCd, String foodNm, String nutConSrtrQua, Double enerc, Double water, Double prot, Double fatce, Double ash, + Double chocdf, Double sugar, Double fibtg, Double ca, Double fe, Double p, Double k, Double nat, Double vitaRae, Double retol, + Double cartb, Double thia, Double ribf, Double nia, Double vitc, Double vitd, Double chole, Double fasat, Double fartn) { this.id = id; this.foodCd = foodCd; this.foodNm = foodNm; diff --git a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java index 9f7921d..e0f44b6 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java +++ b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java @@ -34,52 +34,52 @@ public record Item( @Schema(description = "영양성분함량기준량", example = "100g") String nutConSrtrQua, @Schema(description = "에너지", example = "90") - double enerc, + Double enerc, @Schema(description = "수분", example = "40.7") - double water, + Double water, @Schema(description = "단백질", example = "18.21") - double prot, + Double prot, @Schema(description = "지방", example = "3.14") - double fatce, + Double fatce, @Schema(description = "회분", example = "1.2") - double ash, + Double ash, @Schema(description = "탄수화물", example = "0.61") - double chocdf, + Double chocdf, @Schema(description = "당류", example = "2.64") - double sugar, + Double sugar, @Schema(description = "식이섬유", example = "21.7") - double fibtg, + Double fibtg, @Schema(description = "칼슘", example = "156") - double ca, + Double ca, @Schema(description = "철", example = "10.4") - double fe, + Double fe, @Schema(description = "인", example = "244") - double p, + Double p, @Schema(description = "칼륨", example = "250") - double k, + Double k, @Schema(description = "나트륨", example = "1300") - double nat, + Double nat, @Schema(description = "비타민 A", example = "1007") - double vitaRae, + Double vitaRae, @Schema(description = "레티놀", example = "12") - double retol, + Double retol, @Schema(description = "베타카르틴", example = "2698") - double cartb, + Double cartb, @Schema(description = "티아민", example = "0.968") - double thia, + Double thia, @Schema(description = "리보플라빈", example = "0.11") - double ribf, + Double ribf, @Schema(description = "니아신", example = "6.3") - double nia, + Double nia, @Schema(description = "비타민 C", example = "1.75") - double vitc, + Double vitc, @Schema(description = "비타민 D", example = "48") - double vitd, + Double vitd, @Schema(description = "콜레스테롤", example = "22.57") - double chole, + Double chole, @Schema(description = "포화지방산", example = "0.83") - double fasat, + Double fasat, @Schema(description = "트랜스지방산", example = "0") - double fartn + Double fartn ) {} } \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java index a2f9bf0..0b0b98c 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java +++ b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java @@ -79,7 +79,7 @@ public static List toEntityList(List item .collect(Collectors.toList()); } - public static RawMaterialResponse.Body of(List list, int pageNo,int numOfRows, int totalCount) { + public static RawMaterialResponse.Body ofList(List list, Integer pageNo, Integer numOfRows, Integer totalCount) { List itemList = list.stream() .map(RawMaterialMapper::of) .collect(Collectors.toList()); From 9fceaf4939b2c16fcc9b30644917b308d9b4e4d6 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 25 Apr 2025 02:35:52 +0900 Subject: [PATCH 067/309] =?UTF-8?q?#17=20refactor:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RawMaterialServiceImpl.java | 11 +- .../rawmaterial/domain/RawMaterial.java | 108 +++++++++--------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java index 7c7aefa..258830d 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java @@ -31,17 +31,20 @@ public class RawMaterialServiceImpl implements RawMaterialService { public RawMaterialResponse.Body callRawMaterialAPI(Integer pageNo, Integer numOfRows) { RawMaterialResponse response = rawMaterialClient.getRawMaterialInfo(serviceKey, pageNo, numOfRows, "json"); - RawMaterialResponse.Response responseDto = response.response(); + RawMaterialResponse.Response innerResponse = response.response(); - List items = responseDto.body().items(); + List items = innerResponse.body().items(); - validateRawMaterial(responseDto, items); + validateRawMaterial(innerResponse, items); + + Integer totalCount = Integer.parseInt(innerResponse.body().totalCount()); List rawMaterials = RawMaterialMapper.toEntityList(items); + // TODO: 전처리 후 저장 // List savedRawMaterials = rawMaterialRepository.saveAll(rawMaterials); - return RawMaterialMapper.ofList(rawMaterials, pageNo, numOfRows, items.size()); + return RawMaterialMapper.ofList(rawMaterials, pageNo, numOfRows, totalCount); } private void validateRawMaterial(RawMaterialResponse.Response response, List items) { diff --git a/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java b/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java index 0c61570..60a32bf 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java +++ b/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java @@ -17,86 +17,86 @@ public class RawMaterial extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column - private String foodCd; + @Column(name = "food_cd") + private String foodCd; // 식품 코드 - @Column - private String foodNm; + @Column(name = "food_nm") + private String foodNm; // 식품명 - @Column - private String nutConSrtrQua; + @Column(name = "nut_con_srtr_qua") + private String nutConSrtrQua; // 영양성분함량기준량 - @Column - private Double enerc; + @Column(name = "enerc") + private Double enerc; // 에너지 - @Column - private Double water; + @Column(name = "water") + private Double water; // 수분 - @Column - private Double prot; + @Column(name = "prot") + private Double prot; // 단백질 - @Column - private Double fatce; + @Column(name = "fatce") + private Double fatce; // 지방 - @Column - private Double ash; + @Column(name = "ash") + private Double ash; // 회분 - @Column - private Double chocdf; + @Column(name = "chocdf") + private Double chocdf; // 탄수화물 - @Column - private Double sugar; + @Column(name = "sugar") + private Double sugar; // 당류 - @Column - private Double fibtg; + @Column(name = "fibtg") + private Double fibtg; // 식이섬유 - @Column - private Double ca; + @Column(name = "ca") + private Double ca; // 칼슘 - @Column - private Double fe; + @Column(name = "fe") + private Double fe; // 철 - @Column - private Double p; + @Column(name = "p") + private Double p; // 인 - @Column - private Double k; + @Column(name = "k") + private Double k; // 칼륨 - @Column - private Double nat; + @Column(name = "nat") + private Double nat; // 나트륨 - @Column - private Double vitaRae; + @Column(name = "vita_rae") + private Double vitaRae; // 비타민 A - @Column - private Double retol; + @Column(name = "retol") + private Double retol; // 레티놀 - @Column - private Double cartb; + @Column(name = "cartb") + private Double cartb; // 베타카르틴 - @Column - private Double thia; + @Column(name = "thia") + private Double thia; // 티아민 - @Column - private Double ribf; + @Column(name = "ribf") + private Double ribf; // 리보플라빈 - @Column - private Double nia; + @Column(name = "nia") + private Double nia; // 니아신 - @Column - private Double vitc; + @Column(name = "vitc") + private Double vitc; // 비타민 C - @Column - private Double vitd; + @Column(name = "vitd") + private Double vitd; // 비타민 D - @Column - private Double chole; + @Column(name = "chole") + private Double chole; // 콜레스테롤 - @Column - private Double fasat; + @Column(name = "fasat") + private Double fasat; // 포화지방산 - @Column - private Double fartn; + @Column(name = "fartn") + private Double fartn; // 트랜스지방산 @Builder private RawMaterial(Long id, String foodCd, String foodNm, String nutConSrtrQua, Double enerc, Double water, Double prot, Double fatce, Double ash, From 13a6dd588fabb162ddf93d61f0f79cd6238760ba Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 27 Apr 2025 00:44:42 +0900 Subject: [PATCH 068/309] =?UTF-8?q?#22=20feat:=20=ED=99=94=EC=9E=A5?= =?UTF-8?q?=ED=92=88=20=EC=A3=BC=EC=9D=98=20=EC=84=B1=EB=B6=84=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cosmetic/application/CosmeticService.java | 8 +++++ .../application/CosmeticServiceImpl.java | 19 ++++++++++ .../dto/response/CosmeticResponse.java | 23 ++++++++++++ .../presentation/CosmeticController.java | 32 +++++++++++++++++ .../presentation/swagger/CosmeticSwagger.java | 27 ++++++++++++++ .../global/dto/response/type/SuccessCode.java | 3 ++ .../infrastructure/OpenAiClient.java | 36 +++++++++++++++++++ 7 files changed, 148 insertions(+) create mode 100644 src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java create mode 100644 src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java create mode 100644 src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java create mode 100644 src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java create mode 100644 src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java new file mode 100644 index 0000000..0514e68 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java @@ -0,0 +1,8 @@ +package com.webeye.backend.cosmetic.application; + +import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; + +public interface CosmeticService { + CosmeticResponse analyzeCosmetic(ImageAnalysisRequest request); +} diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java new file mode 100644 index 0000000..de88627 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java @@ -0,0 +1,19 @@ +package com.webeye.backend.cosmetic.application; + +import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CosmeticServiceImpl implements CosmeticService { + + private final OpenAiClient openAiClient; + + @Override + public CosmeticResponse analyzeCosmetic(ImageAnalysisRequest request) { + return openAiClient.explainCosmetic(request); + } +} diff --git a/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java b/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java new file mode 100644 index 0000000..e68bbde --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java @@ -0,0 +1,23 @@ +package com.webeye.backend.cosmetic.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "화장품 주의 성분") +public record CosmeticResponse( + boolean amylCinnamal, + boolean benzylAlcohol, + boolean cinnamylAlcohol, + boolean citral, + boolean eugenol, + boolean hydroxycitronellal, + boolean isoeugenol, + boolean amylCinnamylAlcohol, + boolean benzylSalicylate, + boolean cinnamal, + boolean coumarin, + boolean geraniol, + boolean hicc, + boolean anisylAlcohol, + boolean benzylCinnamate +) { +} diff --git a/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java b/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java new file mode 100644 index 0000000..10dde08 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java @@ -0,0 +1,32 @@ +package com.webeye.backend.cosmetic.presentation; + +import com.webeye.backend.cosmetic.application.CosmeticService; +import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.cosmetic.presentation.swagger.CosmeticSwagger; +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.COSMETIC_ANALYSIS_SUCCESS; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/cosmetic") +public class CosmeticController implements CosmeticSwagger { + + private final CosmeticService cosmeticService; + + @Override + @ResponseStatus(HttpStatus.OK) + @PostMapping + public SuccessResponse analyzeCosmetic(@Valid @RequestBody ImageAnalysisRequest request) { + return SuccessResponse.of(COSMETIC_ANALYSIS_SUCCESS, cosmeticService.analyzeCosmetic(request)); + } +} diff --git a/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java b/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java new file mode 100644 index 0000000..11a2d36 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java @@ -0,0 +1,27 @@ +package com.webeye.backend.cosmetic.presentation.swagger; + +import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "[화장품 주의 성분]", description = "화장품 주의 성분 관련 API") +public interface CosmeticSwagger { + @Operation( + summary = "화장품 주의 원료 성분 추출", + description = "해당 화장품에 대한 주의 원료 성분을 추출합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "화장품 주의 성분이 성공적으로 추출되었습니다." + ) + }) + SuccessResponse analyzeCosmetic( + @RequestBody ImageAnalysisRequest request + ); +} diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index cd0312e..483a472 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -19,6 +19,9 @@ public enum SuccessCode { // allergy ALLERGY_ANALYSIS_SUCCESS(200, "Allergy analysis success"), + // cosmetic + COSMETIC_ANALYSIS_SUCCESS(200, "Cosmetic analysis success"), + // explanation PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS(200, "Product point explanation analysis success"), PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS(200, "Product detail explanation analysis success"), diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index a6cc788..133e571 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -1,6 +1,7 @@ package com.webeye.backend.imageanalysis.infrastructure; import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.global.error.BusinessException; @@ -106,6 +107,41 @@ public NutritionResponse explainNutrition(ImageAnalysisRequest request) { return callWithStructuredOutput(request, prompt, NutritionResponse.class); } + public CosmeticResponse explainCosmetic(ImageAnalysisRequest request) { + String system = """ + You are a cosmetic ingredients description assistant. + """; + + String user = """ + Check the attached image for cosmetic ingredients. + + Use this mapping: + { + "아밀신남알": "amylCinnamal", + "벤질알코올": "benzylAlcohol", + "신나밀알코올": "cinnamylAlcohol", + "시트랄": "citral", + "유제놀": "eugenol", + "하이드록시시트로넬알": "hydroxycitronellal", + "이소유제놀": "isoeugenol", + "아밀신나밀알코올": "amylCinnamylAlcohol", + "벤질살리실레이트": "benzylSalicylate", + "신남알": "cinnamal", + "쿠마린": "coumarin", + "제라니올": "geraniol", + "하이드록시이소헥실3-사이클로헥센카복스알데하이드": "hydroxyisohexyl3CyclohexeneCarboxaldehyde", + "아니스에탄올": "anisylAlcohol", + "벤질신나메이트": "benzylCinnamate" + } + Return a JSON where keys are English names and values are true if the exact full Korean ingredient name appears continuously, false otherwise. + Ignore partial, similar, or incomplete matches. + + Note: "hicc" and "Hydroxyisohexyl 3-Cyclohexene Carboxaldehyde" are the same. + """; + + ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); + return callWithStructuredOutput(request, prompt, CosmeticResponse.class); + } private T callWithStructuredOutput(ImageAnalysisRequest request, ImageAnalysisPrompt prompt, Class clazz) { BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); From 2731f8ef3b0ef05b5c1d843f615c95e763f9b857 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 27 Apr 2025 00:50:43 +0900 Subject: [PATCH 069/309] =?UTF-8?q?#22=20refactor:=20schema=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cosmetic/dto/response/CosmeticResponse.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java b/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java index e68bbde..7d9f2d4 100644 --- a/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java +++ b/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java @@ -4,20 +4,35 @@ @Schema(description = "화장품 주의 성분") public record CosmeticResponse( + @Schema(description = "아밀신남알") boolean amylCinnamal, + @Schema(description = "벤질알코올") boolean benzylAlcohol, + @Schema(description = "신나밀알코올") boolean cinnamylAlcohol, + @Schema(description = "시트랄") boolean citral, + @Schema(description = "유제놀") boolean eugenol, + @Schema(description = "하이드록시시트로넬알") boolean hydroxycitronellal, + @Schema(description = "이소유제놀") boolean isoeugenol, + @Schema(description = "아밀신나밀알코올") boolean amylCinnamylAlcohol, + @Schema(description = "벤질살리실레이트") boolean benzylSalicylate, + @Schema(description = "신남알") boolean cinnamal, + @Schema(description = "쿠마린") boolean coumarin, + @Schema(description = "제라니올") boolean geraniol, + @Schema(description = "하이드록시이소헥실3-사이클로헥센카복스알데하이드") boolean hicc, + @Schema(description = "아니스에탄올") boolean anisylAlcohol, + @Schema(description = "벤질신나메이트") boolean benzylCinnamate ) { } From 2ad7ba24bdef48a900f05e2acc18c6ca4800a529 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 27 Apr 2025 02:22:48 +0900 Subject: [PATCH 070/309] =?UTF-8?q?#22=20refactor:=20=ED=94=84=EB=A1=AC?= =?UTF-8?q?=ED=94=84=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../imageanalysis/infrastructure/OpenAiClient.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 133e571..37bcf89 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -38,7 +38,7 @@ public PointExplanationResponse explainProductPoint(ImageAnalysisRequest request You may adjust the content depending on the product type, but make sure the extracted items are helpful for making a purchase decision. Limit the result to a maximum of 6 items, and keep each one clear and concise. Below is just a guideline — instead of following the content exactly, follow the intent. - + Example (Generate appropriately according to the product) - 핵심 특징 요약 (제품 포인트) - 당류 비교 (10g당 당류 함량 비교) @@ -75,7 +75,7 @@ public AllergyResponse explainAllergy(ImageAnalysisRequest request) { """; String user = """ - Step 1: Carefully examine the attached image(s). + Step 1: Carefully examine the attached image(s). If there is a table that clearly describes '원재료명' (ingredients name), identify it. Do not output anything unless the table exists. @@ -133,10 +133,10 @@ public CosmeticResponse explainCosmetic(ImageAnalysisRequest request) { "아니스에탄올": "anisylAlcohol", "벤질신나메이트": "benzylCinnamate" } - Return a JSON where keys are English names and values are true if the exact full Korean ingredient name appears continuously, false otherwise. + Return true only if the exact full Korean ingredient name appears continuously and separately; otherwise, return false. Ignore partial, similar, or incomplete matches. - Note: "hicc" and "Hydroxyisohexyl 3-Cyclohexene Carboxaldehyde" are the same. + Note: "hicc" and "Hydroxyisohexyl 3-Cyclohexene Carboxaldehyde" are the same. """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); From adc41a1e44ed6fcc70668f78e3674dcf65b6a341 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 27 Apr 2025 23:32:10 +0900 Subject: [PATCH 071/309] =?UTF-8?q?#22=20refactor:=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?api=20=EA=B2=BD=EB=A1=9C=EC=97=90=20=EB=B2=84=EC=A0=84=EB=AA=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/allergy/presentation/AllergyController.java | 2 +- .../backend/explanation/presentation/ExplanationController.java | 2 +- .../backend/nutrition/presentation/NutritionController.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java index 4ae6d8a..a57dec3 100644 --- a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java +++ b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java @@ -14,7 +14,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/allergy") +@RequestMapping("/v1/allergy") public class AllergyController implements AllergySwagger { private final AllergyService allergyService; diff --git a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java index 087054f..72b7111 100644 --- a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java +++ b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java @@ -16,7 +16,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/explanation") +@RequestMapping("/v1/explanation") public class ExplanationController implements ExplanationSwagger { private final ExplanationService explanationService; diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java index f943dd1..82b7334 100644 --- a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java +++ b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java @@ -14,7 +14,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/nutrition") +@RequestMapping("/v1/nutrition") public class NutritionController implements NutritionSwagger { private final NutritionService nutritionService; From 4e490969fac8679dd366e0eb034a554663ea5f8c Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 1 May 2025 02:50:24 +0900 Subject: [PATCH 072/309] #23 feat: clova x client --- .../infrastructure/clovaX/ClovaXClient.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java index e8ed35f..1d53f32 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java @@ -1,4 +1,24 @@ package com.webeye.backend.review.infrastructure.clovaX; -public class ClovaXClient { +import com.webeye.backend.global.config.OpenFeignConfig; +import com.webeye.backend.review.dto.response.ReviewResponse; +import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXRequest; +import jakarta.validation.Valid; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +@FeignClient( + name = "clovaXClient", + url = "${clova.url}", + configuration = OpenFeignConfig.class +) +public interface ClovaXClient { + + @PostMapping() + ReviewResponse createReviewSummary( + @RequestHeader("Authorization") String authorization, + @RequestHeader("X-NCP-CLOVASTUDIO-REQUEST-ID") String requestId, + @RequestHeader("Content-Type") String contentType, + @RequestBody @Valid ClovaXRequest request + ); } From 7cfbded0dd46cfc6d67a5753ca04d051402b5397 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 05:48:35 +0900 Subject: [PATCH 073/309] =?UTF-8?q?#27=20feat:=20openai=20=EC=95=8C?= =?UTF-8?q?=EB=A0=88=EB=A5=B4=EA=B8=B0=20=EC=9C=A0=EB=B0=9C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20true/false=EB=A1=9C=20=EB=B0=9B=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/AllergyAiResponse.java | 94 +++++++++++++++++++ .../allergy/dto/response/AllergyResponse.java | 11 --- 2 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/webeye/backend/allergy/dto/response/AllergyAiResponse.java delete mode 100644 src/main/java/com/webeye/backend/allergy/dto/response/AllergyResponse.java diff --git a/src/main/java/com/webeye/backend/allergy/dto/response/AllergyAiResponse.java b/src/main/java/com/webeye/backend/allergy/dto/response/AllergyAiResponse.java new file mode 100644 index 0000000..166dd83 --- /dev/null +++ b/src/main/java/com/webeye/backend/allergy/dto/response/AllergyAiResponse.java @@ -0,0 +1,94 @@ +package com.webeye.backend.allergy.dto.response; + +import com.webeye.backend.allergy.type.AllergyType; +import lombok.Builder; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "제품 알레르기 유발 성분 응답") +@Builder +public record AllergyAiResponse( + @Schema(description = "계란 함유 여부") + boolean egg, + + @Schema(description = "우유 함유 여부") + boolean milk, + + @Schema(description = "메밀 함유 여부") + boolean buckwheat, + + @Schema(description = "땅콩 함유 여부") + boolean peanut, + + @Schema(description = "대두 함유 여부") + boolean soybean, + + @Schema(description = "밀 함유 여부") + boolean wheat, + + @Schema(description = "잣 함유 여부") + boolean pineNut, + + @Schema(description = "호두 함유 여부") + boolean walnut, + + @Schema(description = "게 함유 여부") + boolean crab, + + @Schema(description = "새우 함유 여부") + boolean shrimp, + + @Schema(description = "오징어 함유 여부") + boolean squid, + + @Schema(description = "고등어 함유 여부") + boolean mackerel, + + @Schema(description = "조개 함유 여부") + boolean shellfish, + + @Schema(description = "복숭아 함유 여부") + boolean peach, + + @Schema(description = "토마토 함유 여부") + boolean tomato, + + @Schema(description = "닭고기 함유 여부") + boolean chicken, + + @Schema(description = "돼지고기 함유 여부") + boolean pork, + + @Schema(description = "쇠고기 함유 여부") + boolean beef, + + @Schema(description = "아황산류 함유 여부") + boolean sulfite +) { + public List getAllergyTypes() { + List result = new ArrayList<>(); + if (egg) result.add(AllergyType.EGG); + if (milk) result.add(AllergyType.MILK); + if (buckwheat) result.add(AllergyType.BUCKWHEAT); + if (peanut) result.add(AllergyType.PEANUT); + if (soybean) result.add(AllergyType.SOYBEAN); + if (wheat) result.add(AllergyType.WHEAT); + if (pineNut) result.add(AllergyType.PINE_NUT); + if (walnut) result.add(AllergyType.WALNUT); + if (crab) result.add(AllergyType.CRAB); + if (shrimp) result.add(AllergyType.SHRIMP); + if (squid) result.add(AllergyType.SQUID); + if (mackerel) result.add(AllergyType.MACKEREL); + if (shellfish) result.add(AllergyType.SHELLFISH); + if (peach) result.add(AllergyType.PEACH); + if (tomato) result.add(AllergyType.TOMATO); + if (chicken) result.add(AllergyType.CHICKEN); + if (pork) result.add(AllergyType.PORK); + if (beef) result.add(AllergyType.BEEF); + if (sulfite) result.add(AllergyType.SULFITE); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/allergy/dto/response/AllergyResponse.java b/src/main/java/com/webeye/backend/allergy/dto/response/AllergyResponse.java deleted file mode 100644 index 3afec47..0000000 --- a/src/main/java/com/webeye/backend/allergy/dto/response/AllergyResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.webeye.backend.allergy.dto.response; - -import lombok.Builder; - -import java.util.List; - -@Builder -public record AllergyResponse ( - List ingredients -) { -} \ No newline at end of file From d8f4cd4967aa7d19b88b12788dce5411b41e8691 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 05:54:58 +0900 Subject: [PATCH 074/309] #27 feat: Allergy, Nutrient, Product entity --- .../backend/allergy/type/AllergyType.java | 23 ++++++++++++ .../backend/disease/domain/Disease.java | 24 ++++++++++++ .../disease/domain/DiseaseNutrient.java | 25 +++++++++++++ .../backend/nutrition/domain/Nutrient.java | 21 +++++++++++ .../nutrition/domain/type/NutrientType.java | 17 +++++++++ .../backend/product/domain/Product.java | 37 +++++++++++++++++++ .../product/domain/ProductAllergy.java | 35 ++++++++++++++++++ .../product/domain/ProductNutrient.java | 37 +++++++++++++++++++ 8 files changed, 219 insertions(+) create mode 100644 src/main/java/com/webeye/backend/allergy/type/AllergyType.java create mode 100644 src/main/java/com/webeye/backend/disease/domain/Disease.java create mode 100644 src/main/java/com/webeye/backend/disease/domain/DiseaseNutrient.java create mode 100644 src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java create mode 100644 src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java create mode 100644 src/main/java/com/webeye/backend/product/domain/Product.java create mode 100644 src/main/java/com/webeye/backend/product/domain/ProductAllergy.java create mode 100644 src/main/java/com/webeye/backend/product/domain/ProductNutrient.java diff --git a/src/main/java/com/webeye/backend/allergy/type/AllergyType.java b/src/main/java/com/webeye/backend/allergy/type/AllergyType.java new file mode 100644 index 0000000..cad73c7 --- /dev/null +++ b/src/main/java/com/webeye/backend/allergy/type/AllergyType.java @@ -0,0 +1,23 @@ +package com.webeye.backend.allergy.type; + +public enum AllergyType { + EGG, // 계란 + MILK, // 우유 + BUCKWHEAT, // 메밀 + PEANUT, // 땅콩 + SOYBEAN, // 대두 + WHEAT, // 밀 + PINE_NUT, // 잣 + WALNUT, // 호두 + CRAB, // 게 + SHRIMP, // 새우 + SQUID, // 오징어 + MACKEREL, // 고등어 + SHELLFISH, // 조개 + PEACH, // 복숭아 + TOMATO, // 토마토 + CHICKEN, // 닭고기 + PORK, // 돼지고기 + BEEF, // 쇠고기 + SULFITE // 아황산류 +} diff --git a/src/main/java/com/webeye/backend/disease/domain/Disease.java b/src/main/java/com/webeye/backend/disease/domain/Disease.java new file mode 100644 index 0000000..9519216 --- /dev/null +++ b/src/main/java/com/webeye/backend/disease/domain/Disease.java @@ -0,0 +1,24 @@ +package com.webeye.backend.disease.domain; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Disease { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + @OneToMany(mappedBy = "disease", cascade = CascadeType.ALL, orphanRemoval = true) + private List diseaseNutrients; +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/disease/domain/DiseaseNutrient.java b/src/main/java/com/webeye/backend/disease/domain/DiseaseNutrient.java new file mode 100644 index 0000000..a23954a --- /dev/null +++ b/src/main/java/com/webeye/backend/disease/domain/DiseaseNutrient.java @@ -0,0 +1,25 @@ +package com.webeye.backend.disease.domain; + +import com.webeye.backend.nutrition.domain.Nutrient; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class DiseaseNutrient { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "disease_id") + private Disease disease; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "nutrient_id") + private Nutrient nutrient; +} diff --git a/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java b/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java new file mode 100644 index 0000000..754aeda --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java @@ -0,0 +1,21 @@ +package com.webeye.backend.nutrition.domain; + +import com.webeye.backend.nutrition.domain.type.NutrientType; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Nutrient { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, unique = true) + private NutrientType type; +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java b/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java new file mode 100644 index 0000000..3dadc7b --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java @@ -0,0 +1,17 @@ +package com.webeye.backend.nutrition.domain.type; + +public enum NutrientType { + SODIUM, + CARBOHYDRATE, + SUGARS, + FAT, + TRANS_FAT, + SATURATED_FAT, + CHOLESTEROL, + PROTEIN, + CALCIUM, + PHOSPHORUS, + NIACIN, + VITAMIN_B, + VITAMIN_E +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java new file mode 100644 index 0000000..4845a75 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -0,0 +1,37 @@ +package com.webeye.backend.product.domain; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Product { + @Id + @Column(name = "product_id") + private String id; // 쿠팡에서 products 뒤에 오는 숫자 + + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + private List allergies = new ArrayList<>(); + + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + private List nutrients = new ArrayList<>(); + + @Builder + public Product(String id) { + this.id = id; + } + + public void addNutrient(ProductNutrient nutrient) { + this.nutrients.add(nutrient); + nutrient.setProduct(this); + } + + public void addAllergy(ProductAllergy allergy) { + this.allergies.add(allergy); + allergy.setProduct(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java b/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java new file mode 100644 index 0000000..dac786d --- /dev/null +++ b/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java @@ -0,0 +1,35 @@ +package com.webeye.backend.product.domain; + +import com.webeye.backend.allergy.type.AllergyType; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductAllergy { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Enumerated(EnumType.STRING) + private AllergyType allergy; + + @Builder + public ProductAllergy(Product product, AllergyType allergy) { + this.product = product; + this.allergy = allergy; + } + + public void setProduct(Product product) { + this.product = product; + product.getAllergies().add(this); + } +} diff --git a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java new file mode 100644 index 0000000..9606fd4 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java @@ -0,0 +1,37 @@ +package com.webeye.backend.product.domain; + +import com.webeye.backend.nutrition.domain.Nutrient; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductNutrient { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "nutrient_id") + private Nutrient nutrient; + + @Column(nullable = false) + private Double amount; // 영양소 함량 + + @Builder + public ProductNutrient(Product product, Nutrient nutrient, Double amount) { + this.product = product; + this.nutrient = nutrient; + this.amount = amount; + } + + public void setProduct(Product product) { + this.product = product; + product.getNutrients().add(this); + } +} \ No newline at end of file From 5e0ae29b3761ff6815f6ac2a627b4d141a6bf314 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 05:56:13 +0900 Subject: [PATCH 075/309] #27 feat: Allergy, Nutrient, Product repository --- .../allergy/persistent/AllergyRepository.java | 4 ++++ .../nutrition/persistent/NutrientRepository.java | 13 +++++++++++++ .../persistent/ProductAllergyRepository.java | 10 ++++++++++ .../persistent/ProductNutrientRepository.java | 9 +++++++++ .../product/persistent/ProductRepository.java | 10 ++++++++++ 5 files changed, 46 insertions(+) create mode 100644 src/main/java/com/webeye/backend/allergy/persistent/AllergyRepository.java create mode 100644 src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java create mode 100644 src/main/java/com/webeye/backend/product/persistent/ProductAllergyRepository.java create mode 100644 src/main/java/com/webeye/backend/product/persistent/ProductNutrientRepository.java create mode 100644 src/main/java/com/webeye/backend/product/persistent/ProductRepository.java diff --git a/src/main/java/com/webeye/backend/allergy/persistent/AllergyRepository.java b/src/main/java/com/webeye/backend/allergy/persistent/AllergyRepository.java new file mode 100644 index 0000000..ea8bdeb --- /dev/null +++ b/src/main/java/com/webeye/backend/allergy/persistent/AllergyRepository.java @@ -0,0 +1,4 @@ +package com.webeye.backend.allergy.persistent; + +public interface AllergyRepository { +} diff --git a/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java new file mode 100644 index 0000000..fe6ba38 --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java @@ -0,0 +1,13 @@ +package com.webeye.backend.nutrition.persistent; + +import com.webeye.backend.nutrition.domain.Nutrient; +import com.webeye.backend.nutrition.domain.type.NutrientType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface NutrientRepository extends JpaRepository { + Optional findByType(NutrientType type); +} diff --git a/src/main/java/com/webeye/backend/product/persistent/ProductAllergyRepository.java b/src/main/java/com/webeye/backend/product/persistent/ProductAllergyRepository.java new file mode 100644 index 0000000..76a1b4f --- /dev/null +++ b/src/main/java/com/webeye/backend/product/persistent/ProductAllergyRepository.java @@ -0,0 +1,10 @@ +package com.webeye.backend.product.persistent; + +import com.webeye.backend.product.domain.ProductAllergy; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductAllergyRepository extends JpaRepository { +} + diff --git a/src/main/java/com/webeye/backend/product/persistent/ProductNutrientRepository.java b/src/main/java/com/webeye/backend/product/persistent/ProductNutrientRepository.java new file mode 100644 index 0000000..026fab2 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/persistent/ProductNutrientRepository.java @@ -0,0 +1,9 @@ +package com.webeye.backend.product.persistent; + +import com.webeye.backend.product.domain.ProductNutrient; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductNutrientRepository extends JpaRepository { +} diff --git a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java new file mode 100644 index 0000000..4254ef2 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java @@ -0,0 +1,10 @@ +package com.webeye.backend.product.persistent; + +import com.webeye.backend.product.domain.Product; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductRepository extends JpaRepository { + boolean existsById(String id); +} From b819e12dd0fbf274f43565ce7b621727bcfa64fb Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 05:58:08 +0900 Subject: [PATCH 076/309] #27 feat: ImageAnalysisRequest -> ProductAnalysisRequest --- .../presentation/AllergyController.java | 6 ++--- .../presentation/swagger/AllergySwagger.java | 9 +++---- .../cosmetic/application/CosmeticService.java | 4 +-- .../application/CosmeticServiceImpl.java | 4 +-- .../presentation/CosmeticController.java | 4 +-- .../presentation/swagger/CosmeticSwagger.java | 4 +-- .../application/ExplanationService.java | 6 ++--- .../presentation/ExplanationController.java | 6 ++--- .../swagger/ExplanationSwagger.java | 7 +++-- .../dto/request/ImageAnalysisRequest.java | 16 ------------ .../presentation/NutritionController.java | 6 ++--- .../swagger/NutritionSwagger.java | 8 +++--- .../dto/request/ProductAnalysisRequest.java | 26 +++++++++++++++++++ 13 files changed, 57 insertions(+), 49 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java create mode 100644 src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java diff --git a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java index a57dec3..c7e6726 100644 --- a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java +++ b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java @@ -1,10 +1,10 @@ package com.webeye.backend.allergy.presentation; import com.webeye.backend.allergy.application.AllergyService; -import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.allergy.dto.response.AllergyAiResponse; import com.webeye.backend.allergy.presentation.swagger.AllergySwagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -21,7 +21,7 @@ public class AllergyController implements AllergySwagger { @Override @ResponseStatus(HttpStatus.OK) @PostMapping(value = "") - public SuccessResponse allergyAnalysis(@Valid @RequestBody ImageAnalysisRequest request) { + public SuccessResponse allergyAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { return SuccessResponse.of(ALLERGY_ANALYSIS_SUCCESS ,allergyService.analyzeAllergy(request)); } } diff --git a/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java b/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java index 5aa1926..dfc28e4 100644 --- a/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java +++ b/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java @@ -1,9 +1,8 @@ package com.webeye.backend.allergy.presentation.swagger; -import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.allergy.dto.response.AllergyAiResponse; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; -import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -22,8 +21,8 @@ public interface AllergySwagger { description = "알러지 유발 요소가 성공적으로 추출되었습니다." ) }) - SuccessResponse allergyAnalysis( - @RequestBody ImageAnalysisRequest request + SuccessResponse allergyAnalysis( + @RequestBody ProductAnalysisRequest request ); } diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java index 0514e68..65f452b 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java @@ -1,8 +1,8 @@ package com.webeye.backend.cosmetic.application; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; public interface CosmeticService { - CosmeticResponse analyzeCosmetic(ImageAnalysisRequest request); + CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request); } diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java index de88627..f2af544 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java @@ -1,7 +1,7 @@ package com.webeye.backend.cosmetic.application; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -13,7 +13,7 @@ public class CosmeticServiceImpl implements CosmeticService { private final OpenAiClient openAiClient; @Override - public CosmeticResponse analyzeCosmetic(ImageAnalysisRequest request) { + public CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request) { return openAiClient.explainCosmetic(request); } } diff --git a/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java b/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java index 10dde08..4772e29 100644 --- a/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java +++ b/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java @@ -4,7 +4,7 @@ import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.cosmetic.presentation.swagger.CosmeticSwagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.parameters.RequestBody; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -26,7 +26,7 @@ public class CosmeticController implements CosmeticSwagger { @Override @ResponseStatus(HttpStatus.OK) @PostMapping - public SuccessResponse analyzeCosmetic(@Valid @RequestBody ImageAnalysisRequest request) { + public SuccessResponse analyzeCosmetic(@Valid @RequestBody ProductAnalysisRequest request) { return SuccessResponse.of(COSMETIC_ANALYSIS_SUCCESS, cosmeticService.analyzeCosmetic(request)); } } diff --git a/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java b/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java index 11a2d36..d6f3dcf 100644 --- a/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java +++ b/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java @@ -2,7 +2,7 @@ import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -22,6 +22,6 @@ public interface CosmeticSwagger { ) }) SuccessResponse analyzeCosmetic( - @RequestBody ImageAnalysisRequest request + @RequestBody ProductAnalysisRequest request ); } diff --git a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java index b938ca8..398ab7b 100644 --- a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java +++ b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java @@ -2,7 +2,7 @@ import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,11 +12,11 @@ public class ExplanationService { private final OpenAiClient openAiClient; - public PointExplanationResponse analyzeProductPoint(ImageAnalysisRequest request) { + public PointExplanationResponse analyzeProductPoint(ProductAnalysisRequest request) { return openAiClient.explainProductPoint(request); } - public DetailExplanationResponse analyzeProductDetail(ImageAnalysisRequest request) { + public DetailExplanationResponse analyzeProductDetail(ProductAnalysisRequest request) { return openAiClient.explainProductDetail(request); } } diff --git a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java index 72b7111..e75dea2 100644 --- a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java +++ b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java @@ -5,7 +5,7 @@ import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.explanation.presentation.swagger.ExplanationSwagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -23,14 +23,14 @@ public class ExplanationController implements ExplanationSwagger { @Override @ResponseStatus(HttpStatus.OK) @PostMapping(value = "") - public SuccessResponse productAnalysis(@Valid @RequestBody ImageAnalysisRequest request) { + public SuccessResponse productAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { return SuccessResponse.of(PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS, explanationService.analyzeProductPoint(request)); } @Override @ResponseStatus(HttpStatus.OK) @PostMapping(value = "/detail") - public SuccessResponse productDetailAnalysis(@Valid @RequestBody ImageAnalysisRequest request) { + public SuccessResponse productDetailAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, explanationService.analyzeProductDetail(request)); } } diff --git a/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java b/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java index 5565aeb..aa212c0 100644 --- a/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java +++ b/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java @@ -3,8 +3,7 @@ import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; -import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -24,7 +23,7 @@ public interface ExplanationSwagger { ) }) SuccessResponse productAnalysis( - @RequestBody ImageAnalysisRequest request + @RequestBody ProductAnalysisRequest request ); @Operation( @@ -38,7 +37,7 @@ SuccessResponse productAnalysis( ) }) SuccessResponse productDetailAnalysis( - @RequestBody ImageAnalysisRequest request + @RequestBody ProductAnalysisRequest request ); } diff --git a/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java b/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java deleted file mode 100644 index 0fc5eec..0000000 --- a/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.webeye.backend.imageanalysis.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import lombok.Builder; - -import java.util.List; - -@Schema(description = "이미지 분석 URL 요청") -@Builder -public record ImageAnalysisRequest ( - @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") - List urls -) { -} - diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java index 82b7334..0e12941 100644 --- a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java +++ b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java @@ -1,9 +1,9 @@ package com.webeye.backend.nutrition.presentation; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.nutrition.application.NutritionService; -import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; import com.webeye.backend.nutrition.presentation.swagger.NutritionSwagger; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -21,7 +21,7 @@ public class NutritionController implements NutritionSwagger { @Override @ResponseStatus(HttpStatus.OK) @PostMapping(value = "") - public SuccessResponse nutritionAnalysis(@Valid @RequestBody ImageAnalysisRequest request) { + public SuccessResponse nutritionAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { return SuccessResponse.of(NUTRITION_ANALYSIS_SUCCESS, nutritionService.analyzeNutrition(request)); } } diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java b/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java index 01de9c1..b6c160b 100644 --- a/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java +++ b/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java @@ -1,8 +1,8 @@ package com.webeye.backend.nutrition.presentation.swagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; -import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -21,7 +21,7 @@ public interface NutritionSwagger { description = "영양소 함량이 성공적으로 추출되었습니다." ) }) - SuccessResponse nutritionAnalysis( - @RequestBody ImageAnalysisRequest request + SuccessResponse nutritionAnalysis( + @RequestBody ProductAnalysisRequest request ); } diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java new file mode 100644 index 0000000..3d37fdf --- /dev/null +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java @@ -0,0 +1,26 @@ +package com.webeye.backend.product.dto.request; + +import com.webeye.backend.allergy.type.AllergyType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; + +import java.util.List; + +@Schema(description = "이미지 분석 URL 요청") +@Builder +public record ProductAnalysisRequest( + @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") + String productId, + + @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") + List urls, + + @Schema(description = "사용자 알레르기") + List allergies, + + @Schema(description = "사용자 질병") + List diseases +) { +} + From 8f344c6c65cfc00d467da4e6f313849f93534800 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 05:58:47 +0900 Subject: [PATCH 077/309] #27 rename: NutritionResponse -> NutritionAiResponse --- .../{NutritionResponse.java => NutritionAiResponse.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/com/webeye/backend/nutrition/dto/response/{NutritionResponse.java => NutritionAiResponse.java} (97%) diff --git a/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java similarity index 97% rename from src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java rename to src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java index 99e29c4..cca48c5 100644 --- a/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionResponse.java +++ b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java @@ -5,7 +5,7 @@ @Schema(description = "제품 영양 정보 응답") @Builder -public record NutritionResponse( +public record NutritionAiResponse( @Schema(description = "나트륨 (mg)", example = "120.5") Double sodium, @Schema(description = "탄수화물 (g)", example = "25.0") From 4283e651cc7b1d08e82c81ee8dbb1389751c902c Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 05:59:32 +0900 Subject: [PATCH 078/309] =?UTF-8?q?#27=20refactor:=20=EC=95=8C=EB=A0=88?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=20=EC=9C=A0=EB=B0=9C=20=EC=9B=90=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=EB=AA=85=20=EC=B6=94=EC=B6=9C=20prompt=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/OpenAiClient.java | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 37bcf89..8b705e2 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -1,13 +1,13 @@ package com.webeye.backend.imageanalysis.infrastructure; -import com.webeye.backend.allergy.dto.response.AllergyResponse; +import com.webeye.backend.allergy.dto.response.AllergyAiResponse; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; -import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; @@ -27,7 +27,7 @@ public class OpenAiClient { private final ChatClient chatClient; - public PointExplanationResponse explainProductPoint(ImageAnalysisRequest request) { + public PointExplanationResponse explainProductPoint(ProductAnalysisRequest request) { String system = """ You are an expert in analyzing product images and extracting key information that helps users make purchase decisions. Based on the image provided, extract only the most relevant and concise information that highlights the product's core value. @@ -52,7 +52,7 @@ public PointExplanationResponse explainProductPoint(ImageAnalysisRequest request } - public DetailExplanationResponse explainProductDetail(ImageAnalysisRequest request) { + public DetailExplanationResponse explainProductDetail(ProductAnalysisRequest request) { String system = """ You are an expert in providing detailed explanations about products based on images. When a user provides a product description image along with the key elements of that description, you should offer a clear and detailed explanation of that element. @@ -69,32 +69,36 @@ public DetailExplanationResponse explainProductDetail(ImageAnalysisRequest reque return callWithStructuredOutput(request, prompt, DetailExplanationResponse.class); } - public AllergyResponse explainAllergy(ImageAnalysisRequest request) { + public AllergyAiResponse explainAllergy(ProductAnalysisRequest request) { String system = """ - You are an assistant that extracts allergy-causing ingredients. + You are an OCR assistant that extracts and detects allergenic ingredients from Korean product label images. Always treat partial matches inside compound words as valid if they contain the full Korean name of an allergen. + """; String user = """ Step 1: Carefully examine the attached image(s). - If there is a table that clearly describes '원재료명' (ingredients name), identify it. - Do not output anything unless the table exists. - - Step 2: From the '원재료명' table, extract all listed ingredients. Do not output anything yet. - - Step 3: Compare the extracted ingredients with the following list: - 계란, 우유, 메밀, 땅콩, 대두, 밀, 잣, 호두, 게, 새우, 오징어, 고등어, 조개, 복숭아, 토마토, 닭고기, 돼지고기, 쇠고기, 아황산류. - - Step 4: If any ingredients from the list are found in the table, return only those names **in Korean**. - Do not explain. If none are found, return nothing. + + Identify the box that contains the full list of ingredients, labeled with '원재료명' or similar (e.g., '원재료 및 함량'). + + Only extract text from this box — ignore all other parts of the image, including allergy warnings or separate notices. + + Step 2: From the identified box, extract the ingredient list **exactly as written**, without summarizing, translating, or omitting anything — including words like '분말', '가루', or '함유'. + + Step 3: Compare the text to the following list of allergenic ingredients: + 계란(EGG), 우유(MILK), 메밀(BUCKWHEAT), 땅콩(PEANUT), 대두(SOYBEAN), 밀(WHEAT), 잣(PINE_NUT), 호두(WALNUT), 게(CRAB), 새우(SHRIMP), 오징어(SQUID), 고등어(MACKEREL), 조개(SHELLFISH), 복숭아(PEACH), 토마토(TOMATO), 닭고기(CHICKEN), 돼지고기(PORK), 쇠고기(BEEF), 아황산류(SULFITE). - Step 5: If '복숭아' is present, be sure to include it in the result — even if it's the only match. - """; + Return true for an allergen if its full Korean name appears **anywhere inside any word** in the ingredient list — even if it is part of a compound word (e.g., "호두함유", "호두분말", "밀가루"). + + Return false if the full Korean allergen word does not appear in any part of the ingredient text. + + Step 4: Output a list of booleans (true/false), in the same order as the allergen list above. + """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request, prompt, AllergyResponse.class); + return callWithStructuredOutput(request, prompt, AllergyAiResponse.class); } - public NutritionResponse explainNutrition(ImageAnalysisRequest request) { + public NutritionAiResponse explainNutrition(ProductAnalysisRequest request) { String system = """ You are a nutrition description assistant. """; @@ -104,10 +108,10 @@ public NutritionResponse explainNutrition(ImageAnalysisRequest request) { """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request, prompt, NutritionResponse.class); + return callWithStructuredOutput(request, prompt, NutritionAiResponse.class); } - public CosmeticResponse explainCosmetic(ImageAnalysisRequest request) { + public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { String system = """ You are a cosmetic ingredients description assistant. """; @@ -143,7 +147,7 @@ public CosmeticResponse explainCosmetic(ImageAnalysisRequest request) { return callWithStructuredOutput(request, prompt, CosmeticResponse.class); } - private T callWithStructuredOutput(ImageAnalysisRequest request, ImageAnalysisPrompt prompt, Class clazz) { + private T callWithStructuredOutput(ProductAnalysisRequest request, ImageAnalysisPrompt prompt, Class clazz) { BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); String response = chatClient.prompt() From d3d5bf89e0495ca502c9896efad761da48b88c56 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 06:02:23 +0900 Subject: [PATCH 079/309] =?UTF-8?q?#27=20feat:=20nutrient=20default=20data?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/global/util/DummyDataInit.java | 14 ++++++++ .../product/persistent/init/NutrientInit.java | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/main/java/com/webeye/backend/global/util/DummyDataInit.java create mode 100644 src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java diff --git a/src/main/java/com/webeye/backend/global/util/DummyDataInit.java b/src/main/java/com/webeye/backend/global/util/DummyDataInit.java new file mode 100644 index 0000000..e6d611c --- /dev/null +++ b/src/main/java/com/webeye/backend/global/util/DummyDataInit.java @@ -0,0 +1,14 @@ +package com.webeye.backend.global.util; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.lang.annotation.*; + +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Transactional +@Component +public @interface DummyDataInit { +} diff --git a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java new file mode 100644 index 0000000..1e07013 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java @@ -0,0 +1,35 @@ +package com.webeye.backend.product.persistent.init; + +import com.webeye.backend.allergy.persistent.AllergyRepository; +import com.webeye.backend.global.util.DummyDataInit; +import com.webeye.backend.nutrition.domain.Nutrient; +import com.webeye.backend.nutrition.domain.type.NutrientType; +import com.webeye.backend.nutrition.persistent.NutrientRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; + +@Slf4j +@RequiredArgsConstructor +@Order(1) +@DummyDataInit +public class NutrientInit implements ApplicationRunner { + private final NutrientRepository nutrientRepository; + + @Override + public void run(ApplicationArguments args) { + if (nutrientRepository.count() > 0) { + log.info("[Nutrient] 기본 데이터 존재"); + } else { + for (NutrientType nutrientType : NutrientType.values()) { + Nutrient nutrient = Nutrient.builder() + .type(nutrientType) + .build(); + nutrientRepository.save(nutrient); + } + log.info("[Nutrient] 기본 데이터 저장 완료"); + } + } +} From 4d4de82fd9d0a5de0b87d22100b4dc4c64dbc090 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 20:18:23 +0900 Subject: [PATCH 080/309] #27 rename: Nutrient.type -> Nutrient.name --- .../backend/nutrition/domain/Nutrient.java | 2 +- .../product/persistent/init/NutrientInit.java | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java b/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java index 754aeda..d2d1cc5 100644 --- a/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java +++ b/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java @@ -17,5 +17,5 @@ public class Nutrient { @Enumerated(EnumType.STRING) @Column(nullable = false, unique = true) - private NutrientType type; + private NutrientType name; } \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java index 1e07013..5ee2fd9 100644 --- a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java +++ b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java @@ -20,16 +20,16 @@ public class NutrientInit implements ApplicationRunner { @Override public void run(ApplicationArguments args) { - if (nutrientRepository.count() > 0) { + if (nutrientRepository.count() >= NutrientType.values().length) { log.info("[Nutrient] 기본 데이터 존재"); - } else { - for (NutrientType nutrientType : NutrientType.values()) { - Nutrient nutrient = Nutrient.builder() - .type(nutrientType) - .build(); - nutrientRepository.save(nutrient); - } - log.info("[Nutrient] 기본 데이터 저장 완료"); + return; } + for (NutrientType nutrientType : NutrientType.values()) { + Nutrient nutrient = Nutrient.builder() + .name(nutrientType) + .build(); + nutrientRepository.save(nutrient); + } + log.info("[Nutrient] 기본 데이터 저장 완료"); } } From de2a36fe46aad5257a8af0b7405d24bc4c1ac24d Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 21:54:02 +0900 Subject: [PATCH 081/309] =?UTF-8?q?#27=20feat:=20=EC=A7=88=EB=B3=91?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=A3=BC=EC=9D=98=20=EC=98=81?= =?UTF-8?q?=EC=96=91=EC=84=B1=EB=B6=84=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/disease/domain/Disease.java | 4 +- .../disease/domain/type/DiseaseType.java | 10 ++ .../persistent/DiseaseNutrientRepository.java | 9 ++ .../disease/persistent/DiseaseRepository.java | 13 ++ .../disease/persistent/init/DiseaseInit.java | 34 ++++++ .../persistent/init/DiseaseNutrientInit.java | 114 ++++++++++++++++++ 6 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/webeye/backend/disease/domain/type/DiseaseType.java create mode 100644 src/main/java/com/webeye/backend/disease/persistent/DiseaseNutrientRepository.java create mode 100644 src/main/java/com/webeye/backend/disease/persistent/DiseaseRepository.java create mode 100644 src/main/java/com/webeye/backend/disease/persistent/init/DiseaseInit.java create mode 100644 src/main/java/com/webeye/backend/disease/persistent/init/DiseaseNutrientInit.java diff --git a/src/main/java/com/webeye/backend/disease/domain/Disease.java b/src/main/java/com/webeye/backend/disease/domain/Disease.java index 9519216..8d14d2b 100644 --- a/src/main/java/com/webeye/backend/disease/domain/Disease.java +++ b/src/main/java/com/webeye/backend/disease/domain/Disease.java @@ -1,5 +1,6 @@ package com.webeye.backend.disease.domain; +import com.webeye.backend.disease.domain.type.DiseaseType; import jakarta.persistence.*; import lombok.*; @@ -16,8 +17,9 @@ public class Disease { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Enumerated(EnumType.STRING) @Column(nullable = false, unique = true) - private String name; + private DiseaseType name; @OneToMany(mappedBy = "disease", cascade = CascadeType.ALL, orphanRemoval = true) private List diseaseNutrients; diff --git a/src/main/java/com/webeye/backend/disease/domain/type/DiseaseType.java b/src/main/java/com/webeye/backend/disease/domain/type/DiseaseType.java new file mode 100644 index 0000000..46ebb95 --- /dev/null +++ b/src/main/java/com/webeye/backend/disease/domain/type/DiseaseType.java @@ -0,0 +1,10 @@ +package com.webeye.backend.disease.domain.type; + +public enum DiseaseType { + DIABETES, // 당뇨 + HYPERTENSION, // 고혈압 + OSTEOPOROSIS, // 골다공증 + KIDNEY_DISEASE, // 신장질환 + GOUT, // 통풍 + FATTY_LIVER // 지방간 +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/disease/persistent/DiseaseNutrientRepository.java b/src/main/java/com/webeye/backend/disease/persistent/DiseaseNutrientRepository.java new file mode 100644 index 0000000..7a8b24c --- /dev/null +++ b/src/main/java/com/webeye/backend/disease/persistent/DiseaseNutrientRepository.java @@ -0,0 +1,9 @@ +package com.webeye.backend.disease.persistent; + +import com.webeye.backend.disease.domain.DiseaseNutrient; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DiseaseNutrientRepository extends JpaRepository { +} diff --git a/src/main/java/com/webeye/backend/disease/persistent/DiseaseRepository.java b/src/main/java/com/webeye/backend/disease/persistent/DiseaseRepository.java new file mode 100644 index 0000000..cb4f398 --- /dev/null +++ b/src/main/java/com/webeye/backend/disease/persistent/DiseaseRepository.java @@ -0,0 +1,13 @@ +package com.webeye.backend.disease.persistent; + +import com.webeye.backend.disease.domain.Disease; +import com.webeye.backend.disease.domain.type.DiseaseType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface DiseaseRepository extends JpaRepository { + Optional findByName(DiseaseType name); +} diff --git a/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseInit.java b/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseInit.java new file mode 100644 index 0000000..074f1c7 --- /dev/null +++ b/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseInit.java @@ -0,0 +1,34 @@ +package com.webeye.backend.disease.persistent.init; + +import com.webeye.backend.disease.domain.Disease; +import com.webeye.backend.disease.domain.type.DiseaseType; +import com.webeye.backend.disease.persistent.DiseaseRepository; +import com.webeye.backend.global.util.DummyDataInit; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; + +@Slf4j +@RequiredArgsConstructor +@Order(1) +@DummyDataInit +public class DiseaseInit implements ApplicationRunner { + private final DiseaseRepository diseaseRepository; + + @Override + public void run(ApplicationArguments args) { + if (diseaseRepository.count() >= DiseaseType.values().length) { + log.info("[Disease] 기본 데이터 존재"); + return; + } + for (DiseaseType diseaseType : DiseaseType.values()) { + Disease disease = Disease.builder() + .name(diseaseType) + .build(); + diseaseRepository.save(disease); + } + log.info("[Disease] 기본 데이터 저장 완료"); + } +} diff --git a/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseNutrientInit.java b/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseNutrientInit.java new file mode 100644 index 0000000..7ee02ce --- /dev/null +++ b/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseNutrientInit.java @@ -0,0 +1,114 @@ +package com.webeye.backend.disease.persistent.init; + +import com.webeye.backend.disease.domain.Disease; +import com.webeye.backend.disease.domain.DiseaseNutrient; +import com.webeye.backend.disease.domain.type.DiseaseType; +import com.webeye.backend.disease.persistent.DiseaseNutrientRepository; +import com.webeye.backend.disease.persistent.DiseaseRepository; +import com.webeye.backend.global.util.DummyDataInit; +import com.webeye.backend.nutrition.domain.Nutrient; +import com.webeye.backend.nutrition.domain.type.NutrientType; +import com.webeye.backend.nutrition.persistent.NutrientRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; + +import java.util.List; + + +@Slf4j +@RequiredArgsConstructor +@Order(2) +@DummyDataInit +public class DiseaseNutrientInit implements ApplicationRunner { + static final int DISEASE_NUTRIENT_NUM = 18; + + private final DiseaseRepository diseaseRepository; + private final NutrientRepository nutrientRepository; + private final DiseaseNutrientRepository diseaseNutrientRepository; + + @Override + public void run(ApplicationArguments args) { + if (diseaseNutrientRepository.count() >= DISEASE_NUTRIENT_NUM) { + log.info("[DiseaseNutrient] 기본 데이터 존재"); + return; + } + + Nutrient sodium = nutrientRepository.findByName(NutrientType.SODIUM) // 나트륨 + .orElseThrow(() -> new RuntimeException("Nutrient sodium not found")); + Nutrient carbohydrate = nutrientRepository.findByName(NutrientType.CARBOHYDRATE) // 탄수화물 + .orElseThrow(() -> new RuntimeException("Nutrient carbohydrate not found")); + Nutrient sugars = nutrientRepository.findByName(NutrientType.SUGARS) // 당류 + .orElseThrow(() -> new RuntimeException("Nutrient sugars not found")); + Nutrient fat = nutrientRepository.findByName(NutrientType.FAT) // 지방 + .orElseThrow(() -> new RuntimeException("Nutrient fat not found")); + Nutrient transFat = nutrientRepository.findByName(NutrientType.TRANS_FAT) // 트랜스지방 + .orElseThrow(() -> new RuntimeException("Nutrient trans fat not found")); + Nutrient saturatedFat = nutrientRepository.findByName(NutrientType.SATURATED_FAT) // 포화지방 + .orElseThrow(() -> new RuntimeException("Nutrient saturated fat not found")); + Nutrient cholesterol = nutrientRepository.findByName(NutrientType.CHOLESTEROL) // 콜레스테롤 + .orElseThrow(() -> new RuntimeException("Nutrient cholesterol not found")); + Nutrient protein = nutrientRepository.findByName(NutrientType.PROTEIN) // 단백질 + .orElseThrow(() -> new RuntimeException("Nutrient protein not found")); + Nutrient calcium = nutrientRepository.findByName(NutrientType.CALCIUM) // 칼슘 + .orElseThrow(() -> new RuntimeException("Nutrient calcium not found")); + Nutrient phosphorus = nutrientRepository.findByName(NutrientType.PHOSPHORUS) // 인 + .orElseThrow(() -> new RuntimeException("Nutrient phosphorus not found")); + Nutrient niacin = nutrientRepository.findByName(NutrientType.NIACIN) // 나이아신 + .orElseThrow(() -> new RuntimeException("Nutrient niacin not found")); + Nutrient vitaminB = nutrientRepository.findByName(NutrientType.VITAMIN_B) // 비타민 B + .orElseThrow(() -> new RuntimeException("Nutrient vitamin B not found")); + Nutrient vitaminE = nutrientRepository.findByName(NutrientType.VITAMIN_E) // 비타민 E + .orElseThrow(() -> new RuntimeException("Nutrient vitamin E not found")); + + + Disease diabetes = diseaseRepository.findByName(DiseaseType.DIABETES) // 당뇨 + .orElseThrow(() -> new RuntimeException("Disease DIABETES not found")); + Disease hypertension = diseaseRepository.findByName(DiseaseType.HYPERTENSION) // 고혈압 + .orElseThrow(() -> new RuntimeException("Disease HYPERTENSION not found")); + Disease osteoporosis = diseaseRepository.findByName(DiseaseType.OSTEOPOROSIS) // 골다공증 + .orElseThrow(() -> new RuntimeException("Disease OSTEOPOROSIS not found")); + Disease kidneyDisease = diseaseRepository.findByName(DiseaseType.KIDNEY_DISEASE) // 신장질환 + .orElseThrow(() -> new RuntimeException("Disease KIDNEY_DISEASE not found")); + Disease gout = diseaseRepository.findByName(DiseaseType.GOUT) // 통풍 + .orElseThrow(() -> new RuntimeException("Disease GOUT not found")); + Disease fattyLiver = diseaseRepository.findByName(DiseaseType.FATTY_LIVER) // 지방간 + .orElseThrow(() -> new RuntimeException("Disease FATTY_LIVER not found")); + + + diseaseNutrientRepository.saveAll(List.of( + // 당뇨: 탄수화물, 당류, 포화지방, 트랜스지방, 콜레스테롤, 나트륨 + DiseaseNutrient.builder().disease(diabetes).nutrient(carbohydrate).build(), + DiseaseNutrient.builder().disease(diabetes).nutrient(sugars).build(), + DiseaseNutrient.builder().disease(diabetes).nutrient(saturatedFat).build(), + DiseaseNutrient.builder().disease(diabetes).nutrient(transFat).build(), + DiseaseNutrient.builder().disease(diabetes).nutrient(cholesterol).build(), + DiseaseNutrient.builder().disease(diabetes).nutrient(sodium).build(), + + // 고혈압: 나트륨, 포화지방, 당류 + DiseaseNutrient.builder().disease(hypertension).nutrient(sodium).build(), + DiseaseNutrient.builder().disease(hypertension).nutrient(saturatedFat).build(), + DiseaseNutrient.builder().disease(hypertension).nutrient(sugars).build(), + + // 골다공증: 나트륨 + DiseaseNutrient.builder().disease(osteoporosis).nutrient(sodium).build(), + + // 신장질환: 나트륨, 인, 콜레스테롤 + DiseaseNutrient.builder().disease(kidneyDisease).nutrient(sodium).build(), + DiseaseNutrient.builder().disease(kidneyDisease).nutrient(phosphorus).build(), + DiseaseNutrient.builder().disease(kidneyDisease).nutrient(cholesterol).build(), + + // 통풍: 단백질, 지방 + DiseaseNutrient.builder().disease(gout).nutrient(protein).build(), + DiseaseNutrient.builder().disease(gout).nutrient(fat).build(), + + // 지방간: 탄수화물, 포화지방, 당류 + DiseaseNutrient.builder().disease(fattyLiver).nutrient(carbohydrate).build(), + DiseaseNutrient.builder().disease(fattyLiver).nutrient(saturatedFat).build(), + DiseaseNutrient.builder().disease(fattyLiver).nutrient(sugars).build() + )); + + } +} From 51ed9ac379220b0f8d14b8a7af0afb3aff24bc7b Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 1 May 2025 23:56:41 +0900 Subject: [PATCH 082/309] =?UTF-8?q?#27=20feat:=20=EC=9D=8C=EC=8B=9D=20?= =?UTF-8?q?=EC=A0=9C=ED=92=88=20=EB=B6=84=EC=84=9D=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/dto/response/ProductResponse.java | 19 +++++++++++++ .../presentation/ProductController.java | 28 +++++++++++++++++++ .../presentation/swagger/ProductSwagger.java | 28 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java create mode 100644 src/main/java/com/webeye/backend/product/presentation/ProductController.java create mode 100644 src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java diff --git a/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java new file mode 100644 index 0000000..85c0915 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java @@ -0,0 +1,19 @@ +package com.webeye.backend.product.dto.response; + +import com.webeye.backend.allergy.type.AllergyType; +import com.webeye.backend.nutrition.domain.type.NutrientType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.util.List; + +@Schema(description = "제품 분석 응답") +@Builder +public record ProductResponse ( + @Schema(description = "포함된 주의 영양성분") + List nutrientTypes, + + @Schema(description = "포함된 알레르기 유발 성분") + List allergyTypes +) { +} diff --git a/src/main/java/com/webeye/backend/product/presentation/ProductController.java b/src/main/java/com/webeye/backend/product/presentation/ProductController.java new file mode 100644 index 0000000..5c7559d --- /dev/null +++ b/src/main/java/com/webeye/backend/product/presentation/ProductController.java @@ -0,0 +1,28 @@ +package com.webeye.backend.product.presentation; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.application.ProductService; +import com.webeye.backend.product.dto.response.ProductResponse; +import com.webeye.backend.product.presentation.swagger.ProductSwagger; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.PRODUCT_ANALYSIS_SUCCESS; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/v1/products") +public class ProductController implements ProductSwagger { + private final ProductService productService; + + @Override + @ResponseStatus(HttpStatus.OK) + @PostMapping(value = "/foods") + public SuccessResponse foodAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { + return SuccessResponse.of(PRODUCT_ANALYSIS_SUCCESS, productService.analyzeFoodProduct(request)); + } + +} diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java new file mode 100644 index 0000000..bb98e7d --- /dev/null +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -0,0 +1,28 @@ +package com.webeye.backend.product.presentation.swagger; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.response.ProductResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "Product", description = "제품 관련 API") +public interface ProductSwagger { + @Operation( + summary = "음식 제품 분석", + description = "음식 제품을 분석합니다. 질환, 알레르기를 입력받아 이를 유발하는 성분이 포함되어 있는지를 반환합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "음식 제품이 성공적으로 분석되었습니다." + ) + }) + SuccessResponse foodAnalysis( + @RequestBody ProductAnalysisRequest request + ); +} From 5d70b8dec123e795e43eed04a5d76fbf01643fbb Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 00:00:09 +0900 Subject: [PATCH 083/309] =?UTF-8?q?#27=20feat:=20=EC=9D=8C=EC=8B=9D=20?= =?UTF-8?q?=EC=A0=9C=ED=92=88=20=EB=B6=84=EC=84=9D=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/global/dto/response/type/SuccessCode.java | 3 +++ .../backend/product/presentation/ProductController.java | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index 483a472..899ca03 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -10,6 +10,9 @@ public enum SuccessCode { // health HEALTH_CHECK_SUCCESS(200, "Health Check Success"), + // product + FOOD_PRODUCT_ANALYSIS_SUCCESS(200, "Product Analysis Success"), + // raw material RAW_MATERIAL_API_SUCCESS(200, "Raw Material API Success"), diff --git a/src/main/java/com/webeye/backend/product/presentation/ProductController.java b/src/main/java/com/webeye/backend/product/presentation/ProductController.java index 5c7559d..f3f8862 100644 --- a/src/main/java/com/webeye/backend/product/presentation/ProductController.java +++ b/src/main/java/com/webeye/backend/product/presentation/ProductController.java @@ -10,7 +10,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import static com.webeye.backend.global.dto.response.type.SuccessCode.PRODUCT_ANALYSIS_SUCCESS; +import static com.webeye.backend.global.dto.response.type.SuccessCode.FOOD_PRODUCT_ANALYSIS_SUCCESS; @RequiredArgsConstructor @RestController @@ -22,7 +22,7 @@ public class ProductController implements ProductSwagger { @ResponseStatus(HttpStatus.OK) @PostMapping(value = "/foods") public SuccessResponse foodAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { - return SuccessResponse.of(PRODUCT_ANALYSIS_SUCCESS, productService.analyzeFoodProduct(request)); + return SuccessResponse.of(FOOD_PRODUCT_ANALYSIS_SUCCESS, productService.analyzeFoodProduct(request)); } } From 70b9a9be7db6fbaa593d6d31c9d5bc4088843765 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 00:08:39 +0900 Subject: [PATCH 084/309] =?UTF-8?q?#27=20feat:=20=EC=9D=8C=EC=8B=9D=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EC=95=8C=EB=A0=88=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=A0=EB=B0=9C=20=EC=84=B1=EB=B6=84=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../allergy/application/AllergyService.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java index 46b6e05..90a6c81 100644 --- a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java +++ b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java @@ -1,18 +1,37 @@ package com.webeye.backend.allergy.application; -import com.webeye.backend.allergy.dto.response.AllergyResponse; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.allergy.dto.response.AllergyAiResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.domain.ProductAllergy; +import com.webeye.backend.product.persistent.ProductRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class AllergyService { private final OpenAiClient openAiClient; + private final ProductRepository productRepository; - public AllergyResponse analyzeAllergy(ImageAnalysisRequest request) { + public AllergyAiResponse analyzeAllergy(ProductAnalysisRequest request) { return openAiClient.explainAllergy(request); } + @Transactional + public void saveProductAllergy(Product product, ProductAnalysisRequest request) { + AllergyAiResponse response = analyzeAllergy(request); + + response.getAllergyTypes().forEach(allergyType -> + product.addAllergy( + ProductAllergy.builder() + .product(product) + .allergy(allergyType) + .build() + ) + ); + productRepository.save(product); + } } From a63252f6f4590beaa7e8285fddb711f5fa5cd4f5 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 00:09:11 +0900 Subject: [PATCH 085/309] =?UTF-8?q?#27=20feat:=20=EC=9D=8C=EC=8B=9D=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EC=98=81=EC=96=91=EC=84=B1=EB=B6=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/global/error/ErrorCode.java | 8 ++- .../application/NutritionService.java | 68 ++++++++++++++++++- .../nutrition/domain/type/NutrientType.java | 26 +++---- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/webeye/backend/global/error/ErrorCode.java b/src/main/java/com/webeye/backend/global/error/ErrorCode.java index 125401f..a162114 100644 --- a/src/main/java/com/webeye/backend/global/error/ErrorCode.java +++ b/src/main/java/com/webeye/backend/global/error/ErrorCode.java @@ -16,7 +16,13 @@ public enum ErrorCode { // open api OPEN_API_RESPONSE_NULL(HttpStatus.INTERNAL_SERVER_ERROR, "Open API 응답에 실패했습니다."), - OPEN_API_DATA_MISSING(HttpStatus.NOT_FOUND, "Open API 데이터가 존재하지 않습니다.") + OPEN_API_DATA_MISSING(HttpStatus.NOT_FOUND, "Open API 데이터가 존재하지 않습니다."), + + // product + PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 제품입니다."), + + // nutrient + NUTRIENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 영양성분입니다."), ; private final HttpStatus status; private final String errorMessage; diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index 56f496f..ed2c3e6 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -1,17 +1,79 @@ package com.webeye.backend.nutrition.application; -import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.global.error.ErrorCode; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; -import com.webeye.backend.nutrition.dto.response.NutritionResponse; +import com.webeye.backend.nutrition.domain.Nutrient; +import com.webeye.backend.nutrition.domain.type.NutrientType; +import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; +import com.webeye.backend.nutrition.persistent.NutrientRepository; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.domain.ProductNutrient; +import com.webeye.backend.product.persistent.ProductRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.EnumMap; +import java.util.Map; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class NutritionService { private final OpenAiClient openAiClient; - public NutritionResponse analyzeNutrition(ImageAnalysisRequest request) { + private final NutrientRepository nutrientRepository; + private final ProductRepository productRepository; + + public NutritionAiResponse analyzeNutrition(ProductAnalysisRequest request) { return openAiClient.explainNutrition(request); } + + public Nutrient findByType(NutrientType type) { + return nutrientRepository.findByName(type) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + } + + @Transactional + public void saveProductNutrition(Product product, ProductAnalysisRequest request) { + NutritionAiResponse response = analyzeNutrition(request); + Map nutrientMap = extractNutrientMap(response); + + nutrientMap.entrySet().stream() + .filter(e -> e.getValue() != null) + .forEach(e -> { + Nutrient nutrient = findByType(e.getKey()); + if (nutrient == null) return; + product.addNutrient( + ProductNutrient.builder() + .product(product) + .nutrient(nutrient) + .amount(e.getValue()) + .build() + ); + }); + productRepository.save(product); + } + + private Map extractNutrientMap(NutritionAiResponse response) { + Map map = new EnumMap<>(NutrientType.class); + + map.put(NutrientType.SODIUM, response.sodium()); + map.put(NutrientType.CARBOHYDRATE, response.carbohydrate()); + map.put(NutrientType.SUGARS, response.sugars()); + map.put(NutrientType.FAT, response.fat()); + map.put(NutrientType.TRANS_FAT, response.transFat()); + map.put(NutrientType.SATURATED_FAT, response.saturatedFat()); + map.put(NutrientType.CHOLESTEROL, response.cholesterol()); + map.put(NutrientType.PROTEIN, response.protein()); + map.put(NutrientType.CALCIUM, response.calcium()); + map.put(NutrientType.PHOSPHORUS, response.phosphorus()); + map.put(NutrientType.NIACIN, response.niacin()); + map.put(NutrientType.VITAMIN_B, response.vitaminB()); + map.put(NutrientType.VITAMIN_E, response.vitaminE()); + + return map; + } } diff --git a/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java b/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java index 3dadc7b..b41dc88 100644 --- a/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java +++ b/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java @@ -1,17 +1,17 @@ package com.webeye.backend.nutrition.domain.type; public enum NutrientType { - SODIUM, - CARBOHYDRATE, - SUGARS, - FAT, - TRANS_FAT, - SATURATED_FAT, - CHOLESTEROL, - PROTEIN, - CALCIUM, - PHOSPHORUS, - NIACIN, - VITAMIN_B, - VITAMIN_E + SODIUM, // 나트룸 + CARBOHYDRATE, // 탄수화물 + SUGARS, // 당류 + FAT, // 지방 + TRANS_FAT, // 트랜스지방 + SATURATED_FAT, // 포화지방 + CHOLESTEROL, // 콜레스테롤 + PROTEIN, // 단백질 + CALCIUM, // 칼슘 + PHOSPHORUS, // 인 + NIACIN, // 나이아신 + VITAMIN_B, // 비타민 B + VITAMIN_E // 비타민 E } \ No newline at end of file From 000db1a46b3693157d7c7eeb0c0d105db8311e05 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 00:09:51 +0900 Subject: [PATCH 086/309] #27 rename: findByType -> findByName --- .../webeye/backend/nutrition/persistent/NutrientRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java index fe6ba38..c5b99d1 100644 --- a/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java +++ b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java @@ -9,5 +9,5 @@ @Repository public interface NutrientRepository extends JpaRepository { - Optional findByType(NutrientType type); + Optional findByName(NutrientType type); } From 422295695332c3fa0a596703d9350c46d8a48d57 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 00:11:30 +0900 Subject: [PATCH 087/309] =?UTF-8?q?#27=20feat:=20=EC=9D=8C=EC=8B=9D=20?= =?UTF-8?q?=EC=A0=9C=ED=92=88=20=EB=B6=84=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/application/ProductService.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/main/java/com/webeye/backend/product/application/ProductService.java diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java new file mode 100644 index 0000000..1fbe128 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -0,0 +1,59 @@ +package com.webeye.backend.product.application; + +import com.webeye.backend.allergy.application.AllergyService; +import com.webeye.backend.allergy.type.AllergyType; +import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.global.error.ErrorCode; +import com.webeye.backend.product.domain.ProductAllergy; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.nutrition.application.NutritionService; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.dto.response.ProductResponse; +import com.webeye.backend.product.persistent.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ProductService { + private final NutritionService nutritionService; + private final AllergyService allergyService; + + private final ProductRepository productRepository; + + // TODO: 음식 제품 영양성분 분석 + @Transactional + public ProductResponse analyzeFoodProduct(ProductAnalysisRequest request) { + if (productRepository.existsById(request.productId())) { + return ProductResponse.builder() + .allergyTypes(getAllergyResponse(request.productId(), request.allergies())) + .build(); + } + + Product product = Product.builder() + .id(request.productId()) + .build(); + productRepository.save(product); + + nutritionService.saveProductNutrition(product, request); + allergyService.saveProductAllergy(product, request); + + return ProductResponse.builder() + .allergyTypes(getAllergyResponse(request.productId(), request.allergies())) + .build(); + } + + private List getAllergyResponse(String productId, List userAllergies) { + Product product = productRepository.findById(productId).orElseThrow(() -> new BusinessException(ErrorCode.PRODUCT_NOT_FOUND)); + + return product.getAllergies().stream() + .map(ProductAllergy::getAllergy) + .distinct() + .filter(userAllergies::contains) + .toList(); + } +} From 3299f6b1ed47e68e8f451d77759cfe7e2d821c6a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 00:19:28 +0900 Subject: [PATCH 088/309] =?UTF-8?q?#27=20remove:=20=EC=A7=88=EB=B3=91=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/disease/domain/Disease.java | 26 ---- .../disease/domain/DiseaseNutrient.java | 25 ---- .../disease/domain/type/DiseaseType.java | 10 -- .../persistent/DiseaseNutrientRepository.java | 9 -- .../disease/persistent/DiseaseRepository.java | 13 -- .../disease/persistent/init/DiseaseInit.java | 34 ------ .../persistent/init/DiseaseNutrientInit.java | 114 ------------------ .../dto/request/ProductAnalysisRequest.java | 5 +- 8 files changed, 1 insertion(+), 235 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/disease/domain/Disease.java delete mode 100644 src/main/java/com/webeye/backend/disease/domain/DiseaseNutrient.java delete mode 100644 src/main/java/com/webeye/backend/disease/domain/type/DiseaseType.java delete mode 100644 src/main/java/com/webeye/backend/disease/persistent/DiseaseNutrientRepository.java delete mode 100644 src/main/java/com/webeye/backend/disease/persistent/DiseaseRepository.java delete mode 100644 src/main/java/com/webeye/backend/disease/persistent/init/DiseaseInit.java delete mode 100644 src/main/java/com/webeye/backend/disease/persistent/init/DiseaseNutrientInit.java diff --git a/src/main/java/com/webeye/backend/disease/domain/Disease.java b/src/main/java/com/webeye/backend/disease/domain/Disease.java deleted file mode 100644 index 8d14d2b..0000000 --- a/src/main/java/com/webeye/backend/disease/domain/Disease.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.webeye.backend.disease.domain; - -import com.webeye.backend.disease.domain.type.DiseaseType; -import jakarta.persistence.*; -import lombok.*; - -import java.util.List; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder -public class Disease { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, unique = true) - private DiseaseType name; - - @OneToMany(mappedBy = "disease", cascade = CascadeType.ALL, orphanRemoval = true) - private List diseaseNutrients; -} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/disease/domain/DiseaseNutrient.java b/src/main/java/com/webeye/backend/disease/domain/DiseaseNutrient.java deleted file mode 100644 index a23954a..0000000 --- a/src/main/java/com/webeye/backend/disease/domain/DiseaseNutrient.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.webeye.backend.disease.domain; - -import com.webeye.backend.nutrition.domain.Nutrient; -import jakarta.persistence.*; -import lombok.*; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder -public class DiseaseNutrient { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "disease_id") - private Disease disease; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "nutrient_id") - private Nutrient nutrient; -} diff --git a/src/main/java/com/webeye/backend/disease/domain/type/DiseaseType.java b/src/main/java/com/webeye/backend/disease/domain/type/DiseaseType.java deleted file mode 100644 index 46ebb95..0000000 --- a/src/main/java/com/webeye/backend/disease/domain/type/DiseaseType.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.webeye.backend.disease.domain.type; - -public enum DiseaseType { - DIABETES, // 당뇨 - HYPERTENSION, // 고혈압 - OSTEOPOROSIS, // 골다공증 - KIDNEY_DISEASE, // 신장질환 - GOUT, // 통풍 - FATTY_LIVER // 지방간 -} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/disease/persistent/DiseaseNutrientRepository.java b/src/main/java/com/webeye/backend/disease/persistent/DiseaseNutrientRepository.java deleted file mode 100644 index 7a8b24c..0000000 --- a/src/main/java/com/webeye/backend/disease/persistent/DiseaseNutrientRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.webeye.backend.disease.persistent; - -import com.webeye.backend.disease.domain.DiseaseNutrient; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface DiseaseNutrientRepository extends JpaRepository { -} diff --git a/src/main/java/com/webeye/backend/disease/persistent/DiseaseRepository.java b/src/main/java/com/webeye/backend/disease/persistent/DiseaseRepository.java deleted file mode 100644 index cb4f398..0000000 --- a/src/main/java/com/webeye/backend/disease/persistent/DiseaseRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.webeye.backend.disease.persistent; - -import com.webeye.backend.disease.domain.Disease; -import com.webeye.backend.disease.domain.type.DiseaseType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface DiseaseRepository extends JpaRepository { - Optional findByName(DiseaseType name); -} diff --git a/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseInit.java b/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseInit.java deleted file mode 100644 index 074f1c7..0000000 --- a/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseInit.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.webeye.backend.disease.persistent.init; - -import com.webeye.backend.disease.domain.Disease; -import com.webeye.backend.disease.domain.type.DiseaseType; -import com.webeye.backend.disease.persistent.DiseaseRepository; -import com.webeye.backend.global.util.DummyDataInit; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.core.annotation.Order; - -@Slf4j -@RequiredArgsConstructor -@Order(1) -@DummyDataInit -public class DiseaseInit implements ApplicationRunner { - private final DiseaseRepository diseaseRepository; - - @Override - public void run(ApplicationArguments args) { - if (diseaseRepository.count() >= DiseaseType.values().length) { - log.info("[Disease] 기본 데이터 존재"); - return; - } - for (DiseaseType diseaseType : DiseaseType.values()) { - Disease disease = Disease.builder() - .name(diseaseType) - .build(); - diseaseRepository.save(disease); - } - log.info("[Disease] 기본 데이터 저장 완료"); - } -} diff --git a/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseNutrientInit.java b/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseNutrientInit.java deleted file mode 100644 index 7ee02ce..0000000 --- a/src/main/java/com/webeye/backend/disease/persistent/init/DiseaseNutrientInit.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.webeye.backend.disease.persistent.init; - -import com.webeye.backend.disease.domain.Disease; -import com.webeye.backend.disease.domain.DiseaseNutrient; -import com.webeye.backend.disease.domain.type.DiseaseType; -import com.webeye.backend.disease.persistent.DiseaseNutrientRepository; -import com.webeye.backend.disease.persistent.DiseaseRepository; -import com.webeye.backend.global.util.DummyDataInit; -import com.webeye.backend.nutrition.domain.Nutrient; -import com.webeye.backend.nutrition.domain.type.NutrientType; -import com.webeye.backend.nutrition.persistent.NutrientRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.core.annotation.Order; - -import java.util.List; - - -@Slf4j -@RequiredArgsConstructor -@Order(2) -@DummyDataInit -public class DiseaseNutrientInit implements ApplicationRunner { - static final int DISEASE_NUTRIENT_NUM = 18; - - private final DiseaseRepository diseaseRepository; - private final NutrientRepository nutrientRepository; - private final DiseaseNutrientRepository diseaseNutrientRepository; - - @Override - public void run(ApplicationArguments args) { - if (diseaseNutrientRepository.count() >= DISEASE_NUTRIENT_NUM) { - log.info("[DiseaseNutrient] 기본 데이터 존재"); - return; - } - - Nutrient sodium = nutrientRepository.findByName(NutrientType.SODIUM) // 나트륨 - .orElseThrow(() -> new RuntimeException("Nutrient sodium not found")); - Nutrient carbohydrate = nutrientRepository.findByName(NutrientType.CARBOHYDRATE) // 탄수화물 - .orElseThrow(() -> new RuntimeException("Nutrient carbohydrate not found")); - Nutrient sugars = nutrientRepository.findByName(NutrientType.SUGARS) // 당류 - .orElseThrow(() -> new RuntimeException("Nutrient sugars not found")); - Nutrient fat = nutrientRepository.findByName(NutrientType.FAT) // 지방 - .orElseThrow(() -> new RuntimeException("Nutrient fat not found")); - Nutrient transFat = nutrientRepository.findByName(NutrientType.TRANS_FAT) // 트랜스지방 - .orElseThrow(() -> new RuntimeException("Nutrient trans fat not found")); - Nutrient saturatedFat = nutrientRepository.findByName(NutrientType.SATURATED_FAT) // 포화지방 - .orElseThrow(() -> new RuntimeException("Nutrient saturated fat not found")); - Nutrient cholesterol = nutrientRepository.findByName(NutrientType.CHOLESTEROL) // 콜레스테롤 - .orElseThrow(() -> new RuntimeException("Nutrient cholesterol not found")); - Nutrient protein = nutrientRepository.findByName(NutrientType.PROTEIN) // 단백질 - .orElseThrow(() -> new RuntimeException("Nutrient protein not found")); - Nutrient calcium = nutrientRepository.findByName(NutrientType.CALCIUM) // 칼슘 - .orElseThrow(() -> new RuntimeException("Nutrient calcium not found")); - Nutrient phosphorus = nutrientRepository.findByName(NutrientType.PHOSPHORUS) // 인 - .orElseThrow(() -> new RuntimeException("Nutrient phosphorus not found")); - Nutrient niacin = nutrientRepository.findByName(NutrientType.NIACIN) // 나이아신 - .orElseThrow(() -> new RuntimeException("Nutrient niacin not found")); - Nutrient vitaminB = nutrientRepository.findByName(NutrientType.VITAMIN_B) // 비타민 B - .orElseThrow(() -> new RuntimeException("Nutrient vitamin B not found")); - Nutrient vitaminE = nutrientRepository.findByName(NutrientType.VITAMIN_E) // 비타민 E - .orElseThrow(() -> new RuntimeException("Nutrient vitamin E not found")); - - - Disease diabetes = diseaseRepository.findByName(DiseaseType.DIABETES) // 당뇨 - .orElseThrow(() -> new RuntimeException("Disease DIABETES not found")); - Disease hypertension = diseaseRepository.findByName(DiseaseType.HYPERTENSION) // 고혈압 - .orElseThrow(() -> new RuntimeException("Disease HYPERTENSION not found")); - Disease osteoporosis = diseaseRepository.findByName(DiseaseType.OSTEOPOROSIS) // 골다공증 - .orElseThrow(() -> new RuntimeException("Disease OSTEOPOROSIS not found")); - Disease kidneyDisease = diseaseRepository.findByName(DiseaseType.KIDNEY_DISEASE) // 신장질환 - .orElseThrow(() -> new RuntimeException("Disease KIDNEY_DISEASE not found")); - Disease gout = diseaseRepository.findByName(DiseaseType.GOUT) // 통풍 - .orElseThrow(() -> new RuntimeException("Disease GOUT not found")); - Disease fattyLiver = diseaseRepository.findByName(DiseaseType.FATTY_LIVER) // 지방간 - .orElseThrow(() -> new RuntimeException("Disease FATTY_LIVER not found")); - - - diseaseNutrientRepository.saveAll(List.of( - // 당뇨: 탄수화물, 당류, 포화지방, 트랜스지방, 콜레스테롤, 나트륨 - DiseaseNutrient.builder().disease(diabetes).nutrient(carbohydrate).build(), - DiseaseNutrient.builder().disease(diabetes).nutrient(sugars).build(), - DiseaseNutrient.builder().disease(diabetes).nutrient(saturatedFat).build(), - DiseaseNutrient.builder().disease(diabetes).nutrient(transFat).build(), - DiseaseNutrient.builder().disease(diabetes).nutrient(cholesterol).build(), - DiseaseNutrient.builder().disease(diabetes).nutrient(sodium).build(), - - // 고혈압: 나트륨, 포화지방, 당류 - DiseaseNutrient.builder().disease(hypertension).nutrient(sodium).build(), - DiseaseNutrient.builder().disease(hypertension).nutrient(saturatedFat).build(), - DiseaseNutrient.builder().disease(hypertension).nutrient(sugars).build(), - - // 골다공증: 나트륨 - DiseaseNutrient.builder().disease(osteoporosis).nutrient(sodium).build(), - - // 신장질환: 나트륨, 인, 콜레스테롤 - DiseaseNutrient.builder().disease(kidneyDisease).nutrient(sodium).build(), - DiseaseNutrient.builder().disease(kidneyDisease).nutrient(phosphorus).build(), - DiseaseNutrient.builder().disease(kidneyDisease).nutrient(cholesterol).build(), - - // 통풍: 단백질, 지방 - DiseaseNutrient.builder().disease(gout).nutrient(protein).build(), - DiseaseNutrient.builder().disease(gout).nutrient(fat).build(), - - // 지방간: 탄수화물, 포화지방, 당류 - DiseaseNutrient.builder().disease(fattyLiver).nutrient(carbohydrate).build(), - DiseaseNutrient.builder().disease(fattyLiver).nutrient(saturatedFat).build(), - DiseaseNutrient.builder().disease(fattyLiver).nutrient(sugars).build() - )); - - } -} diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java index 3d37fdf..092cc6a 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java @@ -17,10 +17,7 @@ public record ProductAnalysisRequest( List urls, @Schema(description = "사용자 알레르기") - List allergies, - - @Schema(description = "사용자 질병") - List diseases + List allergies ) { } From 4205870979015263175eb5e2c0aa5e6888bf0d7e Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 00:22:31 +0900 Subject: [PATCH 089/309] =?UTF-8?q?#27=20refactor:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/product/persistent/init/NutrientInit.java | 1 - .../backend/product/presentation/swagger/ProductSwagger.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java index 5ee2fd9..bd087f0 100644 --- a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java +++ b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java @@ -1,6 +1,5 @@ package com.webeye.backend.product.persistent.init; -import com.webeye.backend.allergy.persistent.AllergyRepository; import com.webeye.backend.global.util.DummyDataInit; import com.webeye.backend.nutrition.domain.Nutrient; import com.webeye.backend.nutrition.domain.type.NutrientType; diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java index bb98e7d..b50f4d4 100644 --- a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -1,7 +1,6 @@ package com.webeye.backend.product.presentation.swagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.product.dto.response.ProductResponse; import io.swagger.v3.oas.annotations.Operation; From 257628e335dea8eeb1ae0a4d07114ed5835d3ddb Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 00:57:40 +0900 Subject: [PATCH 090/309] =?UTF-8?q?#27=20remove:=20Allergy=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/allergy/persistent/AllergyRepository.java | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/allergy/persistent/AllergyRepository.java diff --git a/src/main/java/com/webeye/backend/allergy/persistent/AllergyRepository.java b/src/main/java/com/webeye/backend/allergy/persistent/AllergyRepository.java deleted file mode 100644 index ea8bdeb..0000000 --- a/src/main/java/com/webeye/backend/allergy/persistent/AllergyRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.webeye.backend.allergy.persistent; - -public interface AllergyRepository { -} From 7b37dab02ad59296f11a94dd336cfff0581e5a3e Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 01:10:30 +0900 Subject: [PATCH 091/309] =?UTF-8?q?#27=20refactor:=20=EC=9D=B4=EB=AF=B8=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=ED=95=98=EB=8A=94=20nutrient=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8=ED=95=98=EA=B3=A0=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/persistent/init/NutrientInit.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java index bd087f0..571fafc 100644 --- a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java +++ b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java @@ -19,16 +19,13 @@ public class NutrientInit implements ApplicationRunner { @Override public void run(ApplicationArguments args) { - if (nutrientRepository.count() >= NutrientType.values().length) { - log.info("[Nutrient] 기본 데이터 존재"); - return; - } for (NutrientType nutrientType : NutrientType.values()) { - Nutrient nutrient = Nutrient.builder() - .name(nutrientType) - .build(); - nutrientRepository.save(nutrient); + if (nutrientRepository.findByName(nutrientType).isEmpty()) { + Nutrient nutrient = Nutrient.builder() + .name(nutrientType) + .build(); + nutrientRepository.save(nutrient); + } } - log.info("[Nutrient] 기본 데이터 저장 완료"); } -} +} \ No newline at end of file From b50d4cac13c2fd68a71b0fcb5cba5b688ab2ee5d Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 01:18:01 +0900 Subject: [PATCH 092/309] =?UTF-8?q?#27=20feat:=20saveProductNutrition=20?= =?UTF-8?q?=EA=B0=80=EB=8F=85=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/NutritionService.java | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index ed2c3e6..09dd94f 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -32,7 +32,7 @@ public NutritionAiResponse analyzeNutrition(ProductAnalysisRequest request) { } public Nutrient findByType(NutrientType type) { - return nutrientRepository.findByName(type) + return nutrientRepository.findByType(type) .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); } @@ -41,19 +41,17 @@ public void saveProductNutrition(Product product, ProductAnalysisRequest request NutritionAiResponse response = analyzeNutrition(request); Map nutrientMap = extractNutrientMap(response); - nutrientMap.entrySet().stream() - .filter(e -> e.getValue() != null) - .forEach(e -> { - Nutrient nutrient = findByType(e.getKey()); - if (nutrient == null) return; - product.addNutrient( - ProductNutrient.builder() - .product(product) - .nutrient(nutrient) - .amount(e.getValue()) - .build() - ); - }); + nutrientMap.forEach((type, amount) -> { + if (amount == null) return; + Nutrient nutrient = findByType(type); + product.addNutrient( + ProductNutrient.builder() + .product(product) + .nutrient(nutrient) + .amount(amount) + .build() + ); + }); productRepository.save(product); } From 4203014273e6ad54724c61cb9340899a9490d136 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 2 May 2025 01:18:15 +0900 Subject: [PATCH 093/309] #27 rename: name -> type --- .../java/com/webeye/backend/nutrition/domain/Nutrient.java | 2 +- .../backend/nutrition/persistent/NutrientRepository.java | 2 +- .../webeye/backend/product/persistent/init/NutrientInit.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java b/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java index d2d1cc5..754aeda 100644 --- a/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java +++ b/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java @@ -17,5 +17,5 @@ public class Nutrient { @Enumerated(EnumType.STRING) @Column(nullable = false, unique = true) - private NutrientType name; + private NutrientType type; } \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java index c5b99d1..fe6ba38 100644 --- a/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java +++ b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRepository.java @@ -9,5 +9,5 @@ @Repository public interface NutrientRepository extends JpaRepository { - Optional findByName(NutrientType type); + Optional findByType(NutrientType type); } diff --git a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java index 571fafc..6d493a5 100644 --- a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java +++ b/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java @@ -20,9 +20,9 @@ public class NutrientInit implements ApplicationRunner { @Override public void run(ApplicationArguments args) { for (NutrientType nutrientType : NutrientType.values()) { - if (nutrientRepository.findByName(nutrientType).isEmpty()) { + if (nutrientRepository.findByType(nutrientType).isEmpty()) { Nutrient nutrient = Nutrient.builder() - .name(nutrientType) + .type(nutrientType) .build(); nutrientRepository.save(nutrient); } From 58bf447c1661571caf56dd3364186ca1ecfcb16b Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 02:45:42 +0900 Subject: [PATCH 094/309] =?UTF-8?q?#23=20feat:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 70 +++++++++++++++++-- .../dto/request/ReviewSummaryRequest.java | 8 +++ .../review/dto/response/ReviewResponse.java | 2 - .../infrastructure/clovaX/ClovaXClient.java | 7 +- .../infrastructure/clovaX/ClovaXService.java | 4 -- .../clovaX/domain/ContentType.java | 12 +++- .../infrastructure/clovaX/domain/Role.java | 10 ++- .../clovaX/dto/request/ClovaXContent.java | 2 +- .../clovaX/dto/request/ClovaXMessage.java | 2 +- .../clovaX/dto/response/ClovaXResponse.java | 20 ++++++ .../review/presentation/ReviewController.java | 15 ++-- .../presentation/swagger/ReviewSwagger.java | 5 +- 12 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java delete mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXService.java create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/response/ClovaXResponse.java diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 5dc6714..b6ca0cf 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -1,17 +1,79 @@ package com.webeye.backend.review.application; -import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.review.dto.request.ReviewSummaryRequest; import com.webeye.backend.review.dto.response.ReviewResponse; +import com.webeye.backend.review.infrastructure.clovaX.ClovaXClient; +import com.webeye.backend.review.infrastructure.clovaX.domain.ContentType; +import com.webeye.backend.review.infrastructure.clovaX.domain.Role; +import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXContent; +import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXMessage; +import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXRequest; +import com.webeye.backend.review.infrastructure.clovaX.dto.response.ClovaXResponse; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor public class ReviewService { - private final OpenAiClient openAiClient; + private final ClovaXClient clovaXClient; + + @Value("${clova.secret-key}") + private String secretKey; + + @Value("${clova.request-id}") + private String requestId; + + public ReviewResponse summarizeReview(ReviewSummaryRequest request) { + String reviewText = String.join("\n", request.reviews()); + + ClovaXRequest clovaXRequest = new ClovaXRequest(List.of( + new ClovaXMessage(Role.SYSTEM, List.of( + new ClovaXContent(ContentType.TEXT, + "너는 사용자의 리뷰들을 읽고 다음 3가지로 요약하는 AI야:" + + "1. 긍정적 내용 요약 (한 문장으로)" + + "2. 부정적 내용 요약 (한 문장으로)" + + "3. 대표 키워드 3개 추출 (한 문장으로, 콤마로 구분)" + + + "결과는 다음 형식으로 반환해:" + + "긍정 리뷰: ..." + + "부정 리뷰: ..." + + "키워드: 키워드1, 키워드2, 키워드3" + ) + )), + new ClovaXMessage(Role.USER, List.of( + new ClovaXContent(ContentType.TEXT, reviewText) + )) + )); + ClovaXResponse clovaXResponse = clovaXClient.createReviewSummary("Bearer "+ secretKey, requestId, clovaXRequest); + + return parseResponse(clovaXResponse.result().message().content()); + } + + private ReviewResponse parseResponse(String content) { + String[] lines = content.split("\n"); + + String positive = ""; + String negative = ""; + List keywords = new ArrayList<>(); + + for (String line : lines) { + if (line.startsWith("긍정 리뷰:")) { + positive = line.replace("긍정 리뷰:", "").trim(); + } else if (line.startsWith("부정 리뷰:")) { + negative = line.replace("부정 리뷰:", "").trim(); + } else if (line.startsWith("키워드:")) { + String[] tokens = line.replace("키워드:", "").split(","); + keywords = Arrays.stream(tokens).map(String::trim).collect(Collectors.toList()); + } + } - public ReviewResponse summarizeReview() { - return null; + return new ReviewResponse(positive, negative, keywords); } } diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java new file mode 100644 index 0000000..ea20f3a --- /dev/null +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -0,0 +1,8 @@ +package com.webeye.backend.review.dto.request; + +import java.util.List; + +public record ReviewSummaryRequest( + List reviews +) { +} diff --git a/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java b/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java index 4834034..6a8e461 100644 --- a/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java +++ b/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java @@ -6,8 +6,6 @@ @Schema(description = "리뷰 요약") public record ReviewResponse( - @Schema(description = "별점", example = "4.5") - Double score, @Schema(description = "긍정 리뷰", example = "맛있다는 평가가 많습니다.") String positive, @Schema(description = "부정 리뷰", example = "배송이 느리다는 평가가 많습니다.") diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java index 1d53f32..54b043d 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClient.java @@ -1,8 +1,8 @@ package com.webeye.backend.review.infrastructure.clovaX; import com.webeye.backend.global.config.OpenFeignConfig; -import com.webeye.backend.review.dto.response.ReviewResponse; import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXRequest; +import com.webeye.backend.review.infrastructure.clovaX.dto.response.ClovaXResponse; import jakarta.validation.Valid; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; @@ -14,11 +14,10 @@ ) public interface ClovaXClient { - @PostMapping() - ReviewResponse createReviewSummary( + @PostMapping + ClovaXResponse createReviewSummary( @RequestHeader("Authorization") String authorization, @RequestHeader("X-NCP-CLOVASTUDIO-REQUEST-ID") String requestId, - @RequestHeader("Content-Type") String contentType, @RequestBody @Valid ClovaXRequest request ); } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXService.java deleted file mode 100644 index 398c07e..0000000 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXService.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.webeye.backend.review.infrastructure.clovaX; - -public class ClovaXService { -} diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java index d418715..417720e 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java @@ -1,5 +1,6 @@ package com.webeye.backend.review.infrastructure.clovaX.domain; +import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; @Getter @@ -7,9 +8,14 @@ public enum ContentType { TEXT("text"), IMAGE_URL("image_url"); - private final String value; + private final String type; - ContentType(String value) { - this.value = value; + ContentType(String type) { + this.type = type; + } + + @JsonValue + public String getType() { + return type; } } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java index d72d332..acc0193 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java @@ -1,5 +1,6 @@ package com.webeye.backend.review.infrastructure.clovaX.domain; +import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; @Getter @@ -8,9 +9,14 @@ public enum Role { USER("user"), ASSISTANT("assistant"); - private final String value; + private final String role; Role(String role) { - this.value = role; + this.role = role; + } + + @JsonValue + public String getRole() { + return role; } } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java index 42a6f98..31fb4fb 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java @@ -3,7 +3,7 @@ import com.webeye.backend.review.infrastructure.clovaX.domain.ContentType; public record ClovaXContent( - ContentType contentType, + ContentType type, String text ) { } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java index 7ffbabd..34a07ea 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java @@ -6,6 +6,6 @@ public record ClovaXMessage( Role role, - List contents + List content ) { } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/response/ClovaXResponse.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/response/ClovaXResponse.java new file mode 100644 index 0000000..bd0e7b1 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/response/ClovaXResponse.java @@ -0,0 +1,20 @@ +package com.webeye.backend.review.infrastructure.clovaX.dto.response; + +public record ClovaXResponse( + Status status, + Result result +) { + public record Status( + String code, + String message + ) {} + + public record Result( + Message message + ) { + public record Message( + String role, + String content + ) {} + } +} diff --git a/src/main/java/com/webeye/backend/review/presentation/ReviewController.java b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java index 4ce7288..b4eb0d9 100644 --- a/src/main/java/com/webeye/backend/review/presentation/ReviewController.java +++ b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java @@ -2,28 +2,27 @@ import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.review.application.ReviewService; +import com.webeye.backend.review.dto.request.ReviewSummaryRequest; import com.webeye.backend.review.dto.response.ReviewResponse; import com.webeye.backend.review.presentation.swagger.ReviewSwagger; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import static com.webeye.backend.global.dto.response.type.SuccessCode.REVIEW_SUMMARY_SUCCESS; @RestController @RequiredArgsConstructor -@RequestMapping("/v1/review/summary") +@RequestMapping("/v1/review") public class ReviewController implements ReviewSwagger { private final ReviewService reviewService; @Override - @PostMapping + @PostMapping("/summary") @ResponseStatus(HttpStatus.OK) - public SuccessResponse summarizeReview() { - return SuccessResponse.of(REVIEW_SUMMARY_SUCCESS, reviewService.summarizeReview()); + public SuccessResponse summarizeReview(@RequestBody @Valid ReviewSummaryRequest request) { + return SuccessResponse.of(REVIEW_SUMMARY_SUCCESS, reviewService.summarizeReview(request)); } } diff --git a/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java b/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java index 0634c2b..c788af1 100644 --- a/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java +++ b/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java @@ -1,11 +1,14 @@ package com.webeye.backend.review.presentation.swagger; import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.review.dto.request.ReviewSummaryRequest; import com.webeye.backend.review.dto.response.ReviewResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.RequestBody; @Tag(name = "[리뷰 요약]", description = "리뷰 요약 및 분석 관련 API") public interface ReviewSwagger { @@ -19,5 +22,5 @@ public interface ReviewSwagger { description = "리뷰 요약 및 분석이 성공적으로 수행되었습니다." ) }) - SuccessResponse summarizeReview(); + SuccessResponse summarizeReview(@RequestBody @Valid ReviewSummaryRequest request); } From aa7f8512c5407d494e40ca5071091a320b86dc89 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 02:47:42 +0900 Subject: [PATCH 095/309] =?UTF-8?q?#23=20rename:=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/review/application/ReviewService.java | 4 ++-- .../infrastructure/clovaX/dto/request/ClovaXContent.java | 2 +- .../infrastructure/clovaX/dto/request/ClovaXMessage.java | 2 +- .../infrastructure/clovaX/{domain => model}/ContentType.java | 2 +- .../review/infrastructure/clovaX/{domain => model}/Role.java | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/com/webeye/backend/review/infrastructure/clovaX/{domain => model}/ContentType.java (83%) rename src/main/java/com/webeye/backend/review/infrastructure/clovaX/{domain => model}/Role.java (83%) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index b6ca0cf..9b3eb5a 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -3,8 +3,8 @@ import com.webeye.backend.review.dto.request.ReviewSummaryRequest; import com.webeye.backend.review.dto.response.ReviewResponse; import com.webeye.backend.review.infrastructure.clovaX.ClovaXClient; -import com.webeye.backend.review.infrastructure.clovaX.domain.ContentType; -import com.webeye.backend.review.infrastructure.clovaX.domain.Role; +import com.webeye.backend.review.infrastructure.clovaX.model.ContentType; +import com.webeye.backend.review.infrastructure.clovaX.model.Role; import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXContent; import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXMessage; import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXRequest; diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java index 31fb4fb..1cee3b0 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXContent.java @@ -1,6 +1,6 @@ package com.webeye.backend.review.infrastructure.clovaX.dto.request; -import com.webeye.backend.review.infrastructure.clovaX.domain.ContentType; +import com.webeye.backend.review.infrastructure.clovaX.model.ContentType; public record ClovaXContent( ContentType type, diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java index 34a07ea..f29795c 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/dto/request/ClovaXMessage.java @@ -1,6 +1,6 @@ package com.webeye.backend.review.infrastructure.clovaX.dto.request; -import com.webeye.backend.review.infrastructure.clovaX.domain.Role; +import com.webeye.backend.review.infrastructure.clovaX.model.Role; import java.util.List; diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/model/ContentType.java similarity index 83% rename from src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java rename to src/main/java/com/webeye/backend/review/infrastructure/clovaX/model/ContentType.java index 417720e..3a407b0 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/ContentType.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/model/ContentType.java @@ -1,4 +1,4 @@ -package com.webeye.backend.review.infrastructure.clovaX.domain; +package com.webeye.backend.review.infrastructure.clovaX.model; import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/model/Role.java similarity index 83% rename from src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java rename to src/main/java/com/webeye/backend/review/infrastructure/clovaX/model/Role.java index acc0193..ce676bc 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/domain/Role.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/model/Role.java @@ -1,4 +1,4 @@ -package com.webeye.backend.review.infrastructure.clovaX.domain; +package com.webeye.backend.review.infrastructure.clovaX.model; import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; From 4c6f9c3b18445c5475ef28b2bac23f52eccf7710 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 02:48:33 +0900 Subject: [PATCH 096/309] #23 rename: reviewResponse -> reviewSummaryresponse --- .../webeye/backend/review/application/ReviewService.java | 8 ++++---- .../{ReviewResponse.java => ReviewSummaryResponse.java} | 2 +- .../backend/review/presentation/ReviewController.java | 4 ++-- .../review/presentation/swagger/ReviewSwagger.java | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/main/java/com/webeye/backend/review/dto/response/{ReviewResponse.java => ReviewSummaryResponse.java} (93%) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 9b3eb5a..5f572b2 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -1,7 +1,7 @@ package com.webeye.backend.review.application; import com.webeye.backend.review.dto.request.ReviewSummaryRequest; -import com.webeye.backend.review.dto.response.ReviewResponse; +import com.webeye.backend.review.dto.response.ReviewSummaryResponse; import com.webeye.backend.review.infrastructure.clovaX.ClovaXClient; import com.webeye.backend.review.infrastructure.clovaX.model.ContentType; import com.webeye.backend.review.infrastructure.clovaX.model.Role; @@ -30,7 +30,7 @@ public class ReviewService { @Value("${clova.request-id}") private String requestId; - public ReviewResponse summarizeReview(ReviewSummaryRequest request) { + public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { String reviewText = String.join("\n", request.reviews()); ClovaXRequest clovaXRequest = new ClovaXRequest(List.of( @@ -56,7 +56,7 @@ public ReviewResponse summarizeReview(ReviewSummaryRequest request) { return parseResponse(clovaXResponse.result().message().content()); } - private ReviewResponse parseResponse(String content) { + private ReviewSummaryResponse parseResponse(String content) { String[] lines = content.split("\n"); String positive = ""; @@ -74,6 +74,6 @@ private ReviewResponse parseResponse(String content) { } } - return new ReviewResponse(positive, negative, keywords); + return new ReviewSummaryResponse(positive, negative, keywords); } } diff --git a/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java similarity index 93% rename from src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java rename to src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java index 6a8e461..57e0695 100644 --- a/src/main/java/com/webeye/backend/review/dto/response/ReviewResponse.java +++ b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java @@ -5,7 +5,7 @@ import java.util.List; @Schema(description = "리뷰 요약") -public record ReviewResponse( +public record ReviewSummaryResponse( @Schema(description = "긍정 리뷰", example = "맛있다는 평가가 많습니다.") String positive, @Schema(description = "부정 리뷰", example = "배송이 느리다는 평가가 많습니다.") diff --git a/src/main/java/com/webeye/backend/review/presentation/ReviewController.java b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java index b4eb0d9..5c1b3c5 100644 --- a/src/main/java/com/webeye/backend/review/presentation/ReviewController.java +++ b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java @@ -3,7 +3,7 @@ import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.review.application.ReviewService; import com.webeye.backend.review.dto.request.ReviewSummaryRequest; -import com.webeye.backend.review.dto.response.ReviewResponse; +import com.webeye.backend.review.dto.response.ReviewSummaryResponse; import com.webeye.backend.review.presentation.swagger.ReviewSwagger; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -22,7 +22,7 @@ public class ReviewController implements ReviewSwagger { @Override @PostMapping("/summary") @ResponseStatus(HttpStatus.OK) - public SuccessResponse summarizeReview(@RequestBody @Valid ReviewSummaryRequest request) { + public SuccessResponse summarizeReview(@RequestBody @Valid ReviewSummaryRequest request) { return SuccessResponse.of(REVIEW_SUMMARY_SUCCESS, reviewService.summarizeReview(request)); } } diff --git a/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java b/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java index c788af1..6784322 100644 --- a/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java +++ b/src/main/java/com/webeye/backend/review/presentation/swagger/ReviewSwagger.java @@ -2,7 +2,7 @@ import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.review.dto.request.ReviewSummaryRequest; -import com.webeye.backend.review.dto.response.ReviewResponse; +import com.webeye.backend.review.dto.response.ReviewSummaryResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -22,5 +22,5 @@ public interface ReviewSwagger { description = "리뷰 요약 및 분석이 성공적으로 수행되었습니다." ) }) - SuccessResponse summarizeReview(@RequestBody @Valid ReviewSummaryRequest request); + SuccessResponse summarizeReview(@RequestBody @Valid ReviewSummaryRequest request); } From 12a854e08205f07bd8ccfb44360ea045659a6423 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 03:02:55 +0900 Subject: [PATCH 097/309] =?UTF-8?q?#23=20refactor:=20reviewService,=20Clov?= =?UTF-8?q?aXClientService=EB=A1=9C=20=EC=97=AD=ED=95=A0=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 66 +--------------- .../clovaX/ClovaXClientService.java | 75 +++++++++++++++++++ 2 files changed, 78 insertions(+), 63 deletions(-) create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 5f572b2..2390eb8 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -2,78 +2,18 @@ import com.webeye.backend.review.dto.request.ReviewSummaryRequest; import com.webeye.backend.review.dto.response.ReviewSummaryResponse; -import com.webeye.backend.review.infrastructure.clovaX.ClovaXClient; -import com.webeye.backend.review.infrastructure.clovaX.model.ContentType; -import com.webeye.backend.review.infrastructure.clovaX.model.Role; -import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXContent; -import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXMessage; -import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXRequest; -import com.webeye.backend.review.infrastructure.clovaX.dto.response.ClovaXResponse; +import com.webeye.backend.review.infrastructure.clovaX.ClovaXClientService; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class ReviewService { - private final ClovaXClient clovaXClient; - - @Value("${clova.secret-key}") - private String secretKey; - - @Value("${clova.request-id}") - private String requestId; + private final ClovaXClientService clovaXClientService; public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { String reviewText = String.join("\n", request.reviews()); - - ClovaXRequest clovaXRequest = new ClovaXRequest(List.of( - new ClovaXMessage(Role.SYSTEM, List.of( - new ClovaXContent(ContentType.TEXT, - "너는 사용자의 리뷰들을 읽고 다음 3가지로 요약하는 AI야:" + - "1. 긍정적 내용 요약 (한 문장으로)" + - "2. 부정적 내용 요약 (한 문장으로)" + - "3. 대표 키워드 3개 추출 (한 문장으로, 콤마로 구분)" + - - "결과는 다음 형식으로 반환해:" + - "긍정 리뷰: ..." + - "부정 리뷰: ..." + - "키워드: 키워드1, 키워드2, 키워드3" - ) - )), - new ClovaXMessage(Role.USER, List.of( - new ClovaXContent(ContentType.TEXT, reviewText) - )) - )); - ClovaXResponse clovaXResponse = clovaXClient.createReviewSummary("Bearer "+ secretKey, requestId, clovaXRequest); - - return parseResponse(clovaXResponse.result().message().content()); - } - - private ReviewSummaryResponse parseResponse(String content) { - String[] lines = content.split("\n"); - - String positive = ""; - String negative = ""; - List keywords = new ArrayList<>(); - - for (String line : lines) { - if (line.startsWith("긍정 리뷰:")) { - positive = line.replace("긍정 리뷰:", "").trim(); - } else if (line.startsWith("부정 리뷰:")) { - negative = line.replace("부정 리뷰:", "").trim(); - } else if (line.startsWith("키워드:")) { - String[] tokens = line.replace("키워드:", "").split(","); - keywords = Arrays.stream(tokens).map(String::trim).collect(Collectors.toList()); - } - } - - return new ReviewSummaryResponse(positive, negative, keywords); + return clovaXClientService.summarizeReviewText(reviewText); } } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java new file mode 100644 index 0000000..fedd8ba --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -0,0 +1,75 @@ +package com.webeye.backend.review.infrastructure.clovaX; + +import com.webeye.backend.review.dto.response.ReviewSummaryResponse; +import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXContent; +import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXMessage; +import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXRequest; +import com.webeye.backend.review.infrastructure.clovaX.dto.response.ClovaXResponse; +import com.webeye.backend.review.infrastructure.clovaX.model.ContentType; +import com.webeye.backend.review.infrastructure.clovaX.model.Role; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ClovaXClientService { + + private final ClovaXClient clovaXClient; + + @Value("${clova.secret-key}") + private String secretKey; + + @Value("${clova.request-id}") + private String requestId; + + public ReviewSummaryResponse summarizeReviewText(String reviewText) { + ClovaXRequest clovaXRequest = new ClovaXRequest(List.of( + new ClovaXMessage(Role.SYSTEM, List.of( + new ClovaXContent(ContentType.TEXT, + "너는 사용자의 리뷰들을 읽고 다음 3가지로 요약하는 AI야:" + + "1. 긍정적 내용 요약 (한 문장으로)" + + "2. 부정적 내용 요약 (한 문장으로)" + + "3. 대표 키워드 3개 추출 (한 문장으로, 콤마로 구분)" + + + "결과는 다음 형식으로 반환해:" + + "긍정 리뷰: ..." + + "부정 리뷰: ..." + + "키워드: 키워드1, 키워드2, 키워드3" + ) + )), + new ClovaXMessage(Role.USER, List.of( + new ClovaXContent(ContentType.TEXT, reviewText) + )) + )); + ClovaXResponse clovaXResponse = clovaXClient.createReviewSummary("Bearer "+ secretKey, requestId, clovaXRequest); + + return parseResponse(clovaXResponse.result().message().content()); + } + + private ReviewSummaryResponse parseResponse(String content) { + String[] lines = content.split("\n"); + + String positive = ""; + String negative = ""; + List keywords = new ArrayList<>(); + + for (String line : lines) { + if (line.startsWith("긍정 리뷰:")) { + positive = line.replace("긍정 리뷰:", "").trim(); + } else if (line.startsWith("부정 리뷰:")) { + negative = line.replace("부정 리뷰:", "").trim(); + } else if (line.startsWith("키워드:")) { + String[] tokens = line.replace("키워드:", "").split(","); + keywords = Arrays.stream(tokens).map(String::trim).collect(Collectors.toList()); + } + } + + return new ReviewSummaryResponse(positive, negative, keywords); + } +} From 8f307fe9c5e990f523fdb4a1477d322c0aa6f602 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 03:05:53 +0900 Subject: [PATCH 098/309] =?UTF-8?q?#23=20refactor:=20schema=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20builder=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/review/dto/request/ReviewSummaryRequest.java | 6 ++++++ .../backend/review/dto/response/ReviewSummaryResponse.java | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index ea20f3a..1e6cd47 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -1,8 +1,14 @@ package com.webeye.backend.review.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + import java.util.List; +@Builder +@Schema(description = "리뷰 목록") public record ReviewSummaryRequest( + @Schema(description = "쿠팡 리뷰 목록", example = "맛있어요, 배송 느려요, 부드러워요") List reviews ) { } diff --git a/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java index 57e0695..6539e49 100644 --- a/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java +++ b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java @@ -1,9 +1,11 @@ package com.webeye.backend.review.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; import java.util.List; +@Builder @Schema(description = "리뷰 요약") public record ReviewSummaryResponse( @Schema(description = "긍정 리뷰", example = "맛있다는 평가가 많습니다.") From 719cae3902d389cd099d9d166ed4629c07279f27 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 03:20:45 +0900 Subject: [PATCH 099/309] #23 feat: review domain --- .../webeye/backend/review/domain/Review.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/java/com/webeye/backend/review/domain/Review.java diff --git a/src/main/java/com/webeye/backend/review/domain/Review.java b/src/main/java/com/webeye/backend/review/domain/Review.java new file mode 100644 index 0000000..5a8b0d2 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/domain/Review.java @@ -0,0 +1,42 @@ +package com.webeye.backend.review.domain; + +import com.webeye.backend.global.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Review extends BaseEntity { + + @Id + @Column(name = "review_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String positiveSummary; + + @Column(nullable = false) + private String negativeSummary; + + @Column(nullable = false) + private String keywords; + + @Builder + public Review(String positiveSummary, String negativeSummary, String keywords) { + this.positiveSummary = positiveSummary; + this.negativeSummary = negativeSummary; + this.keywords = keywords; + } + + public List getKeywordList() { + return Arrays.asList(this.keywords.split(",")); + } +} From a3a43b1623a2ff4a55a3be055fe41364d1381fce Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 03:38:18 +0900 Subject: [PATCH 100/309] =?UTF-8?q?#23=20feat:=20review=20db=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 12 ++++++++- .../infrastructure/mapper/ReviewMapper.java | 25 +++++++++++++++++++ .../persistence/ReviewRepository.java | 7 ++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/persistence/ReviewRepository.java diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 2390eb8..cdcc056 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -1,8 +1,11 @@ package com.webeye.backend.review.application; +import com.webeye.backend.review.domain.Review; import com.webeye.backend.review.dto.request.ReviewSummaryRequest; import com.webeye.backend.review.dto.response.ReviewSummaryResponse; import com.webeye.backend.review.infrastructure.clovaX.ClovaXClientService; +import com.webeye.backend.review.infrastructure.mapper.ReviewMapper; +import com.webeye.backend.review.infrastructure.persistence.ReviewRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -11,9 +14,16 @@ public class ReviewService { private final ClovaXClientService clovaXClientService; + public final ReviewRepository reviewRepository; public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { String reviewText = String.join("\n", request.reviews()); - return clovaXClientService.summarizeReviewText(reviewText); + + ReviewSummaryResponse response = clovaXClientService.summarizeReviewText(reviewText); + + Review review = ReviewMapper.toEntity(response); + reviewRepository.save(review); + + return response; } } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java new file mode 100644 index 0000000..8ac7862 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java @@ -0,0 +1,25 @@ +package com.webeye.backend.review.infrastructure.mapper; + +import com.webeye.backend.review.domain.Review; +import com.webeye.backend.review.dto.response.ReviewSummaryResponse; + +import java.util.Arrays; + +public class ReviewMapper { + + public static Review toEntity(ReviewSummaryResponse response) { + return Review.builder() + .positiveSummary(response.positive()) + .negativeSummary(response.negative()) + .keywords(String.join(",", response.keywords())) + .build(); + } + + public static ReviewSummaryResponse toResponse(Review review) { + return new ReviewSummaryResponse( + review.getPositiveSummary(), + review.getNegativeSummary(), + Arrays.asList(review.getKeywords().split(",")) + ); + } +} diff --git a/src/main/java/com/webeye/backend/review/infrastructure/persistence/ReviewRepository.java b/src/main/java/com/webeye/backend/review/infrastructure/persistence/ReviewRepository.java new file mode 100644 index 0000000..f54fd1c --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/persistence/ReviewRepository.java @@ -0,0 +1,7 @@ +package com.webeye.backend.review.infrastructure.persistence; + +import com.webeye.backend.review.domain.Review; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewRepository extends JpaRepository { +} From f28f4c14edc185ba6268b118c98afe87d3f5be0d Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 03:42:01 +0900 Subject: [PATCH 101/309] =?UTF-8?q?#23=20fix:=20createdAt,=20updatedAt=20n?= =?UTF-8?q?ull=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/BackendApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/webeye/backend/BackendApplication.java b/src/main/java/com/webeye/backend/BackendApplication.java index 6366ab2..7515743 100644 --- a/src/main/java/com/webeye/backend/BackendApplication.java +++ b/src/main/java/com/webeye/backend/BackendApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class BackendApplication { From ef186270e8da4d2d6f89c2ef9073147781a6ef09 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 03:45:33 +0900 Subject: [PATCH 102/309] =?UTF-8?q?#23=20refactor:=20transactional=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 --- .../com/webeye/backend/review/application/ReviewService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index cdcc056..cb7eb30 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -8,6 +8,7 @@ import com.webeye.backend.review.infrastructure.persistence.ReviewRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -16,6 +17,7 @@ public class ReviewService { private final ClovaXClientService clovaXClientService; public final ReviewRepository reviewRepository; + @Transactional public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { String reviewText = String.join("\n", request.reviews()); From c17fa8d8262982ed53e7550c3d97809e941dc4c9 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 03:48:22 +0900 Subject: [PATCH 103/309] =?UTF-8?q?#23=20docs:=20todo=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/review/application/ReviewService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index cb7eb30..110a83b 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -28,4 +28,6 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { return response; } + + // TODO: 저장된 리뷰 요약 조회 } From 2f6701c2b520c3f2b534b7b0fcde9b6dbc622659 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 2 May 2025 03:53:18 +0900 Subject: [PATCH 104/309] =?UTF-8?q?#23=20feat:=20product=20column=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 --- src/main/java/com/webeye/backend/review/domain/Review.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/webeye/backend/review/domain/Review.java b/src/main/java/com/webeye/backend/review/domain/Review.java index 5a8b0d2..9db3904 100644 --- a/src/main/java/com/webeye/backend/review/domain/Review.java +++ b/src/main/java/com/webeye/backend/review/domain/Review.java @@ -29,11 +29,16 @@ public class Review extends BaseEntity { @Column(nullable = false) private String keywords; +// @OneToOne(fetch = FetchType.LAZY) +// @JoinColumn(name = "product_id", unique = true) +// private Product product; + @Builder public Review(String positiveSummary, String negativeSummary, String keywords) { this.positiveSummary = positiveSummary; this.negativeSummary = negativeSummary; this.keywords = keywords; +// this.product = product; } public List getKeywordList() { From 64025cf91b056bfe7ec3f6266725040677dbbda4 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 3 May 2025 00:45:18 +0900 Subject: [PATCH 105/309] =?UTF-8?q?#23=20refactor:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 2 +- .../dto/request/ReviewSummaryRequest.java | 2 +- .../clovaX/ClovaXClientService.java | 25 +++++++++++-------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 110a83b..0c866b3 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -15,7 +15,7 @@ public class ReviewService { private final ClovaXClientService clovaXClientService; - public final ReviewRepository reviewRepository; + private final ReviewRepository reviewRepository; @Transactional public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 1e6cd47..164bdf3 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -8,7 +8,7 @@ @Builder @Schema(description = "리뷰 목록") public record ReviewSummaryRequest( - @Schema(description = "쿠팡 리뷰 목록", example = "맛있어요, 배송 느려요, 부드러워요") + @Schema(description = "쿠팡 리뷰 목록", example = "[맛있어요, 배송 느려요, 부드러워요]") List reviews ) { } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index fedd8ba..0f803be 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -33,14 +33,19 @@ public ReviewSummaryResponse summarizeReviewText(String reviewText) { new ClovaXMessage(Role.SYSTEM, List.of( new ClovaXContent(ContentType.TEXT, "너는 사용자의 리뷰들을 읽고 다음 3가지로 요약하는 AI야:" + - "1. 긍정적 내용 요약 (한 문장으로)" + - "2. 부정적 내용 요약 (한 문장으로)" + - "3. 대표 키워드 3개 추출 (한 문장으로, 콤마로 구분)" + + "1. 긍정적 내용 요약 (한 문장으로)" + + "2. 부정적 내용 요약 (한 문장으로)" + + "3. 대표 키워드 3개 추출 (한 문장으로, 콤마로 구분)" + - "결과는 다음 형식으로 반환해:" + - "긍정 리뷰: ..." + - "부정 리뷰: ..." + - "키워드: 키워드1, 키워드2, 키워드3" + "결과는 다음 형식으로 반환해:" + + "긍정 리뷰: ..." + + "부정 리뷰: ..." + + "키워드: 키워드 1, 키워드 2, 키워드 3" + + + "예시:" + + "긍정 리뷰: 맛있다는 평가가 많습니다." + + "부정 리뷰: 배송이 느리다는 평가가 많습니다." + + "키워드: 맛있어요, 신선해요, 배송이 느려요" ) )), new ClovaXMessage(Role.USER, List.of( @@ -60,11 +65,11 @@ private ReviewSummaryResponse parseResponse(String content) { List keywords = new ArrayList<>(); for (String line : lines) { - if (line.startsWith("긍정 리뷰:")) { + if (line != null && line.startsWith("긍정 리뷰:")) { positive = line.replace("긍정 리뷰:", "").trim(); - } else if (line.startsWith("부정 리뷰:")) { + } else if (line != null && line.startsWith("부정 리뷰:")) { negative = line.replace("부정 리뷰:", "").trim(); - } else if (line.startsWith("키워드:")) { + } else if (line != null && line.startsWith("키워드:")) { String[] tokens = line.replace("키워드:", "").split(","); keywords = Arrays.stream(tokens).map(String::trim).collect(Collectors.toList()); } From 187b10fdbec577e525157f669be9c8bd26bbf1d4 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 3 May 2025 18:46:48 +0900 Subject: [PATCH 106/309] =?UTF-8?q?#31=20feat:=20=EA=B1=B4=EA=B0=95?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=8B=9D=ED=92=88=20DB=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/healthfood/domain/HealthFood.java | 36 +++++++++++++++++++ .../backend/healthfood/domain/Keyword.java | 4 +++ 2 files changed, 40 insertions(+) create mode 100644 src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java create mode 100644 src/main/java/com/webeye/backend/healthfood/domain/Keyword.java diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java new file mode 100644 index 0000000..23c2b72 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java @@ -0,0 +1,36 @@ +package com.webeye.backend.healthfood.domain; + +import com.webeye.backend.global.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HealthFood extends BaseEntity { + + @Id + @Column(name = "health_food_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String functionality; + + @Column + @Enumerated(EnumType.STRING) + private Keyword keyword; + + @Builder + public HealthFood(String name, String functionality, Keyword keyword) { + this.name = name; + this.functionality = functionality; + this.keyword = keyword; + } +} diff --git a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java new file mode 100644 index 0000000..1c190f6 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java @@ -0,0 +1,4 @@ +package com.webeye.backend.healthfood.domain; + +public enum Keyword { +} From aa99aaa41dbedbd68961d1edb30d899eec489de2 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 3 May 2025 23:09:20 +0900 Subject: [PATCH 107/309] =?UTF-8?q?#31=20feat:=20=EA=B1=B4=EA=B0=95?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=8B=9D=ED=92=88=20Open=20API=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/dto/response/type/SuccessCode.java | 3 ++ .../application/HealthFoodService.java | 38 +++++++++++++++++++ .../healthfood/dto/HealthFoodResponse.java | 20 ++++++++++ .../client/HealthFoodClient.java | 24 ++++++++++++ .../mapper/HealthFoodMapper.java | 38 +++++++++++++++++++ .../presentation/HealthFoodController.java | 29 ++++++++++++++ .../swagger/HealthFoodSwagger.java | 23 +++++++++++ 7 files changed, 175 insertions(+) create mode 100644 src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java create mode 100644 src/main/java/com/webeye/backend/healthfood/dto/HealthFoodResponse.java create mode 100644 src/main/java/com/webeye/backend/healthfood/infrastructure/client/HealthFoodClient.java create mode 100644 src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java create mode 100644 src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java create mode 100644 src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index 483a472..2fc9ac5 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -26,6 +26,9 @@ public enum SuccessCode { PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS(200, "Product point explanation analysis success"), PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS(200, "Product detail explanation analysis success"), + // health food + HEALTH_FOOD_API_SUCCESS(200, "Health Food API success"), + ; private final int status; diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java new file mode 100644 index 0000000..6c6bd81 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -0,0 +1,38 @@ +package com.webeye.backend.healthfood.application; + +import com.webeye.backend.healthfood.domain.HealthFood; +import com.webeye.backend.healthfood.dto.HealthFoodResponse; +import com.webeye.backend.healthfood.infrastructure.client.HealthFoodClient; +import com.webeye.backend.healthfood.infrastructure.mapper.HealthFoodMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class HealthFoodService { + + @Value("${open-api.health-food.service-id}") + private String serviceId; + + @Value("${open-api.health-food.service-key}") + private String serviceKey; + + private final HealthFoodClient healthFoodClient; + + @Transactional + public HealthFoodResponse.I2710 callHealthFoodApi() { + HealthFoodResponse response = healthFoodClient.getHealthFood(serviceKey, serviceId, "json", "1", "478"); + + HealthFoodResponse.I2710 i2710 = response.I2710(); + + List rows = i2710.row(); + + List healthFoods = HealthFoodMapper.toEntityList(rows); + + return HealthFoodMapper.toResponseList(healthFoods, i2710.totalCount()); + } +} diff --git a/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodResponse.java b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodResponse.java new file mode 100644 index 0000000..36fa3cc --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodResponse.java @@ -0,0 +1,20 @@ +package com.webeye.backend.healthfood.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record HealthFoodResponse( + I2710 I2710 +) { + public record I2710( + @JsonProperty("total_count") + Integer totalCount, + List row + ){} + + public record Row( + String PRDCT_NM, + String PRIMARY_FNCLTY + ){} +} diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/client/HealthFoodClient.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/client/HealthFoodClient.java new file mode 100644 index 0000000..03695bc --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/client/HealthFoodClient.java @@ -0,0 +1,24 @@ +package com.webeye.backend.healthfood.infrastructure.client; + +import com.webeye.backend.global.config.OpenFeignConfig; +import com.webeye.backend.healthfood.dto.HealthFoodResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient( + name = "healthFoodClient", + url = "${open-api.health-food.url}", + configuration = OpenFeignConfig.class +) +public interface HealthFoodClient { + + @GetMapping("/{keyId}/{serviceId}/{dataType}/{startIdx}/{endIdx}") + HealthFoodResponse getHealthFood( + @PathVariable("keyId") String keyId, + @PathVariable("serviceId") String serviceId, + @PathVariable("dataType") String dataType, + @PathVariable("startIdx") String startIdx, + @PathVariable("endIdx") String endIdx + ); +} diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java new file mode 100644 index 0000000..b8e3e09 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java @@ -0,0 +1,38 @@ +package com.webeye.backend.healthfood.infrastructure.mapper; + +import com.webeye.backend.healthfood.domain.HealthFood; +import com.webeye.backend.healthfood.dto.HealthFoodResponse; + +import java.util.List; +import java.util.stream.Collectors; + +public class HealthFoodMapper { + + public static HealthFood toEntity(HealthFoodResponse.Row row) { + return HealthFood.builder() + .name(row.PRDCT_NM()) + .functionality(row.PRIMARY_FNCLTY()) + .build(); + } + + public static List toEntityList(List rows) { + return rows.stream() + .map(HealthFoodMapper::toEntity) + .collect(Collectors.toList()); + } + + public static HealthFoodResponse.Row toResponse(HealthFood healthFood) { + return new HealthFoodResponse.Row( + healthFood.getName(), + healthFood.getFunctionality() + ); + } + + public static HealthFoodResponse.I2710 toResponseList(List healthFoodList, Integer totalCount) { + List i2710List = healthFoodList.stream() + .map(HealthFoodMapper::toResponse) + .collect(Collectors.toList()); + + return new HealthFoodResponse.I2710(totalCount, i2710List); + } +} diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java new file mode 100644 index 0000000..218b0b6 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java @@ -0,0 +1,29 @@ +package com.webeye.backend.healthfood.presentation; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.healthfood.application.HealthFoodService; +import com.webeye.backend.healthfood.dto.HealthFoodResponse; +import com.webeye.backend.healthfood.presentation.swagger.HealthFoodSwagger; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.HEALTH_FOOD_API_SUCCESS; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/health-food") +public class HealthFoodController implements HealthFoodSwagger { + + private final HealthFoodService healthFoodService; + + @Override + @ResponseStatus(HttpStatus.OK) + @GetMapping + public SuccessResponse callHealthFoodApi() { + return SuccessResponse.of(HEALTH_FOOD_API_SUCCESS, healthFoodService.callHealthFoodApi()); + } +} diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java b/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java new file mode 100644 index 0000000..081878f --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java @@ -0,0 +1,23 @@ +package com.webeye.backend.healthfood.presentation.swagger; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.healthfood.dto.HealthFoodResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "[건강 기능 식품]", description = "건강 기능 식품 관련 API") +public interface HealthFoodSwagger { + @Operation( + summary = "건강 기능 식품 OPEN API 호출", + description = "식품 안전 나라 건강 기능 식품 품목 분류 정보 OPEN API를 호출합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OPEN API가 성공적으로 호출되었습니다." + ) + }) + SuccessResponse callHealthFoodApi(); +} From f19ae0d5bffc52dbe20dc9a5d13684960bff755c Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 3 May 2025 23:10:32 +0900 Subject: [PATCH 108/309] =?UTF-8?q?#31=20refactor:=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/RawMaterialServiceImpl.java | 2 +- .../rawmaterial/infrastructure/client/RawMaterialClient.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java index 258830d..45d2eec 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java @@ -20,7 +20,7 @@ @RequiredArgsConstructor public class RawMaterialServiceImpl implements RawMaterialService { - @Value("${open-api.service-key}") + @Value("${open-api.raw-material.service-key}") private String serviceKey; private final RawMaterialClient rawMaterialClient; diff --git a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java index 1875f54..a9e1e97 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java +++ b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java @@ -8,13 +8,13 @@ @FeignClient( name = "rawMaterialClient", - url = "${open-api.url}", + url = "${open-api.raw-material.url}", configuration = OpenFeignConfig.class ) public interface RawMaterialClient { @GetMapping( - value = "/openapi/tn_pubr_public_nutri_material_info_api", + value = "${open-api.raw-material.value}", produces = "application/json" ) RawMaterialResponse getRawMaterialInfo( From 496fa812f93822b52e1e0d0aad1a9914cfbc027f Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 3 May 2025 23:46:48 +0900 Subject: [PATCH 109/309] =?UTF-8?q?#31=20feat:=20DB=20=EA=B5=AC=EC=B6=95?= =?UTF-8?q?=20=EB=B0=8F=20=EC=BB=AC=EB=9F=BC=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../healthfood/application/HealthFoodService.java | 2 ++ .../webeye/backend/healthfood/domain/HealthFood.java | 12 ++++++------ .../webeye/backend/healthfood/domain/Keyword.java | 1 + .../infrastructure/mapper/HealthFoodMapper.java | 4 ++-- .../persistence/HealthFoodRepository.java | 7 +++++++ 5 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 6c6bd81..5b9c1e3 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -4,6 +4,7 @@ import com.webeye.backend.healthfood.dto.HealthFoodResponse; import com.webeye.backend.healthfood.infrastructure.client.HealthFoodClient; import com.webeye.backend.healthfood.infrastructure.mapper.HealthFoodMapper; +import com.webeye.backend.healthfood.infrastructure.persistence.HealthFoodRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -22,6 +23,7 @@ public class HealthFoodService { private String serviceKey; private final HealthFoodClient healthFoodClient; + private final HealthFoodRepository healthFoodRepository; @Transactional public HealthFoodResponse.I2710 callHealthFoodApi() { diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java index 23c2b72..543cb37 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java @@ -18,19 +18,19 @@ public class HealthFood extends BaseEntity { private Long id; @Column(nullable = false) - private String name; + private String itemName; - @Column(nullable = false) + @Column(nullable = false, columnDefinition = "TEXT") private String functionality; @Column @Enumerated(EnumType.STRING) - private Keyword keyword; + private Keyword keywords; @Builder - public HealthFood(String name, String functionality, Keyword keyword) { - this.name = name; + public HealthFood(String itemName, String functionality, Keyword keywords) { + this.itemName = itemName; this.functionality = functionality; - this.keyword = keyword; + this.keywords = keywords; } } diff --git a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java index 1c190f6..72cb5a1 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java @@ -1,4 +1,5 @@ package com.webeye.backend.healthfood.domain; public enum Keyword { + KEYWORD } diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java index b8e3e09..63470b0 100644 --- a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java @@ -10,7 +10,7 @@ public class HealthFoodMapper { public static HealthFood toEntity(HealthFoodResponse.Row row) { return HealthFood.builder() - .name(row.PRDCT_NM()) + .itemName(row.PRDCT_NM()) .functionality(row.PRIMARY_FNCLTY()) .build(); } @@ -23,7 +23,7 @@ public static List toEntityList(List rows) { public static HealthFoodResponse.Row toResponse(HealthFood healthFood) { return new HealthFoodResponse.Row( - healthFood.getName(), + healthFood.getItemName(), healthFood.getFunctionality() ); } diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java new file mode 100644 index 0000000..8ecf8da --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java @@ -0,0 +1,7 @@ +package com.webeye.backend.healthfood.infrastructure.persistence; + +import com.webeye.backend.healthfood.domain.HealthFood; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface HealthFoodRepository extends JpaRepository { +} From 5de78d0e229b4af462bd084c453a0d1d213e2afa Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sun, 4 May 2025 01:13:30 +0900 Subject: [PATCH 110/309] =?UTF-8?q?#27=20refactor:=20Builder=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/nutrition/domain/Nutrient.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java b/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java index 754aeda..6136e99 100644 --- a/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java +++ b/src/main/java/com/webeye/backend/nutrition/domain/Nutrient.java @@ -7,10 +7,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder public class Nutrient { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -18,4 +15,9 @@ public class Nutrient { @Enumerated(EnumType.STRING) @Column(nullable = false, unique = true) private NutrientType type; + + @Builder + public Nutrient(NutrientType type) { + this.type = type; + } } \ No newline at end of file From eab164ca671e9b1337079ac0ad92804c00e28f83 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sun, 4 May 2025 01:19:39 +0900 Subject: [PATCH 111/309] #27 rename: setProduct -> associateWithProduct --- src/main/java/com/webeye/backend/product/domain/Product.java | 4 ++-- .../com/webeye/backend/product/domain/ProductAllergy.java | 2 +- .../com/webeye/backend/product/domain/ProductNutrient.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 4845a75..0efa674 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -27,11 +27,11 @@ public Product(String id) { public void addNutrient(ProductNutrient nutrient) { this.nutrients.add(nutrient); - nutrient.setProduct(this); + nutrient.associateWithProduct(this); } public void addAllergy(ProductAllergy allergy) { this.allergies.add(allergy); - allergy.setProduct(this); + allergy.associateWithProduct(this); } } \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java b/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java index dac786d..ba7bdbf 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java +++ b/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java @@ -28,7 +28,7 @@ public ProductAllergy(Product product, AllergyType allergy) { this.allergy = allergy; } - public void setProduct(Product product) { + public void associateWithProduct(Product product) { this.product = product; product.getAllergies().add(this); } diff --git a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java index 9606fd4..86cf3e6 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java +++ b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java @@ -30,7 +30,7 @@ public ProductNutrient(Product product, Nutrient nutrient, Double amount) { this.amount = amount; } - public void setProduct(Product product) { + public void associateWithProduct(Product product) { this.product = product; product.getNutrients().add(this); } From e69452dd2d56cf1be5596e5265d5e4dfe1277763 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 5 May 2025 00:26:14 +0900 Subject: [PATCH 112/309] =?UTF-8?q?#32=20docs:=20Nutrient=20=ED=95=9C?= =?UTF-8?q?=EA=B5=AD=EC=96=B4=20=EC=9D=B4=EB=A6=84=20swagger=EC=97=90=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 --- .../webeye/backend/product/dto/response/ProductResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java index 85c0915..d813444 100644 --- a/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java +++ b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java @@ -7,7 +7,7 @@ import java.util.List; -@Schema(description = "제품 분석 응답") +@Schema(description = "제품 분석 응답 (SODIUM: 나트륨, CARBOHYDRATE: 탄수화물, SUGARS: 당류, FAT: 지방, TRANS_FAT: 트랜스지방, SATURATED_FAT: 포화지방, CHOLESTEROL: 콜레스테롤, PROTEIN: 단백질, CALCIUM: 칼슘, PHOSPHORUS: 인, NIACIN: 나이아신, VITAMIN_B: 비타민 B, VITAMIN_E: 비타민 E)") @Builder public record ProductResponse ( @Schema(description = "포함된 주의 영양성분") From 66b99614cb468204f43c1915145497e7a6368f70 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 5 May 2025 00:34:54 +0900 Subject: [PATCH 113/309] =?UTF-8?q?#32=20refactor:=20NutrientInit=20packag?= =?UTF-8?q?e=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{product => nutrition}/persistent/init/NutrientInit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/com/webeye/backend/{product => nutrition}/persistent/init/NutrientInit.java (95%) diff --git a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java b/src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientInit.java similarity index 95% rename from src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java rename to src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientInit.java index 6d493a5..78b382a 100644 --- a/src/main/java/com/webeye/backend/product/persistent/init/NutrientInit.java +++ b/src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientInit.java @@ -1,4 +1,4 @@ -package com.webeye.backend.product.persistent.init; +package com.webeye.backend.nutrition.persistent.init; import com.webeye.backend.global.util.DummyDataInit; import com.webeye.backend.nutrition.domain.Nutrient; From a3f4e89efef1a4773e12ed6d1347ef3a1ea7de0b Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 5 May 2025 00:45:22 +0900 Subject: [PATCH 114/309] =?UTF-8?q?#32=20feat:=20=EC=98=81=EC=96=91?= =?UTF-8?q?=EC=84=B1=EB=B6=84=20=EA=B6=8C=EC=9E=A5=EB=9F=89=20entity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/NutrientRecommendation.java | 50 +++++++++++++++++++ .../backend/nutrition/domain/type/Gender.java | 6 +++ 2 files changed, 56 insertions(+) create mode 100644 src/main/java/com/webeye/backend/nutrition/domain/NutrientRecommendation.java create mode 100644 src/main/java/com/webeye/backend/nutrition/domain/type/Gender.java diff --git a/src/main/java/com/webeye/backend/nutrition/domain/NutrientRecommendation.java b/src/main/java/com/webeye/backend/nutrition/domain/NutrientRecommendation.java new file mode 100644 index 0000000..8e7cadd --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/domain/NutrientRecommendation.java @@ -0,0 +1,50 @@ +package com.webeye.backend.nutrition.domain; + +import com.webeye.backend.nutrition.domain.type.Gender; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class NutrientRecommendation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "nutrient_id", nullable = false) + private Nutrient nutrient; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Gender gender; + + @Column(nullable = false) + private int minAge; + + @Column(nullable = false) + private int maxAge; + + @Column(nullable = false) + private double amount; + + @Builder + public NutrientRecommendation( + Nutrient nutrient, + Gender gender, + int minAge, + int maxAge, + double amount + ) { + this.nutrient = nutrient; + this.gender = gender; + this.minAge = minAge; + this.maxAge = maxAge; + this.amount = amount; + } +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/nutrition/domain/type/Gender.java b/src/main/java/com/webeye/backend/nutrition/domain/type/Gender.java new file mode 100644 index 0000000..1ea2e57 --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/domain/type/Gender.java @@ -0,0 +1,6 @@ +package com.webeye.backend.nutrition.domain.type; + +public enum Gender { + MALE, + FEMALE +} \ No newline at end of file From 3a719a1c015051c277c36dc585f25fdeb17cd648 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 5 May 2025 00:47:37 +0900 Subject: [PATCH 115/309] #28 feat: NutrientRecommendationRepository --- .../persistent/NutrientRecommendationRepository.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/java/com/webeye/backend/nutrition/persistent/NutrientRecommendationRepository.java diff --git a/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRecommendationRepository.java b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRecommendationRepository.java new file mode 100644 index 0000000..76758dc --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRecommendationRepository.java @@ -0,0 +1,9 @@ +package com.webeye.backend.nutrition.persistent; + +import com.webeye.backend.nutrition.domain.NutrientRecommendation; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NutrientRecommendationRepository extends JpaRepository { +} From b0d37effdc1ac6c179b2661579f1f35520be46cb Mon Sep 17 00:00:00 2001 From: yeonjy Date: Tue, 6 May 2025 01:58:33 +0900 Subject: [PATCH 116/309] =?UTF-8?q?#32=20refactor:=20=EB=82=98=ED=8A=B8?= =?UTF-8?q?=EB=A3=B8=20->=20=EB=82=98=ED=8A=B8=EB=A5=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/nutrition/domain/type/NutrientType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java b/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java index b41dc88..f09c871 100644 --- a/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java +++ b/src/main/java/com/webeye/backend/nutrition/domain/type/NutrientType.java @@ -1,7 +1,7 @@ package com.webeye.backend.nutrition.domain.type; public enum NutrientType { - SODIUM, // 나트룸 + SODIUM, // 나트륨 CARBOHYDRATE, // 탄수화물 SUGARS, // 당류 FAT, // 지방 From cb2177acc1dfb09d346130a04aea66e5dd0993d7 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 6 May 2025 22:31:26 +0900 Subject: [PATCH 117/309] =?UTF-8?q?#31=20refactor:=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/healthfood/domain/HealthFood.java | 1 + .../java/com/webeye/backend/healthfood/domain/Keyword.java | 5 ----- .../com/webeye/backend/healthfood/domain/type/Keyword.java | 5 +++++ 3 files changed, 6 insertions(+), 5 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/healthfood/domain/Keyword.java create mode 100644 src/main/java/com/webeye/backend/healthfood/domain/type/Keyword.java diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java index 543cb37..dc52832 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java @@ -1,6 +1,7 @@ package com.webeye.backend.healthfood.domain; import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.healthfood.domain.type.Keyword; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java deleted file mode 100644 index 72cb5a1..0000000 --- a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.webeye.backend.healthfood.domain; - -public enum Keyword { - KEYWORD -} diff --git a/src/main/java/com/webeye/backend/healthfood/domain/type/Keyword.java b/src/main/java/com/webeye/backend/healthfood/domain/type/Keyword.java new file mode 100644 index 0000000..18e8db0 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/domain/type/Keyword.java @@ -0,0 +1,5 @@ +package com.webeye.backend.healthfood.domain.type; + +public enum Keyword { + KEYWORD +} From 02326bb82ac6a9d39486a476a8ce6de21e0aa876 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Tue, 6 May 2025 22:49:24 +0900 Subject: [PATCH 118/309] =?UTF-8?q?#32=20feat:=20NutrientRecommendation=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../init/NutrientRecommendationInit.java | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientRecommendationInit.java diff --git a/src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientRecommendationInit.java b/src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientRecommendationInit.java new file mode 100644 index 0000000..c582e1c --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientRecommendationInit.java @@ -0,0 +1,301 @@ +package com.webeye.backend.nutrition.persistent.init; + +import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.global.error.ErrorCode; +import com.webeye.backend.global.util.DummyDataInit; +import com.webeye.backend.nutrition.domain.Nutrient; +import com.webeye.backend.nutrition.domain.NutrientRecommendation; +import com.webeye.backend.nutrition.domain.type.Gender; +import com.webeye.backend.nutrition.domain.type.NutrientType; +import com.webeye.backend.nutrition.persistent.NutrientRecommendationRepository; +import com.webeye.backend.nutrition.persistent.NutrientRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; + +@Slf4j +@RequiredArgsConstructor +@Order(2) +@DummyDataInit +public class NutrientRecommendationInit implements ApplicationRunner { + + private final NutrientRecommendationRepository recommendationRepository; + private final NutrientRepository nutrientRepository; + + @Override + public void run(ApplicationArguments args) { + if (recommendationRepository.count() > 0) { + return; + } + + Nutrient carbohydrate = nutrientRepository.findByType(NutrientType.CARBOHYDRATE) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + Nutrient protein = nutrientRepository.findByType(NutrientType.PROTEIN) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + Nutrient calcium = nutrientRepository.findByType(NutrientType.CALCIUM) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + Nutrient sodium = nutrientRepository.findByType(NutrientType.SODIUM) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + Nutrient vitaminE = nutrientRepository.findByType(NutrientType.VITAMIN_E) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + Nutrient vitaminB = nutrientRepository.findByType(NutrientType.VITAMIN_B) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + Nutrient fat = nutrientRepository.findByType(NutrientType.FAT) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + Nutrient transFat = nutrientRepository.findByType(NutrientType.TRANS_FAT) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + Nutrient saturatedFat = nutrientRepository.findByType(NutrientType.SATURATED_FAT) + .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); + + // 탄수화물 권장 섭취량 + saveRecommendation(carbohydrate, Gender.MALE, 0, 0, 75); + saveRecommendation(carbohydrate, Gender.MALE, 1, 2, 130); + saveRecommendation(carbohydrate, Gender.MALE, 3, 5, 130); + saveRecommendation(carbohydrate, Gender.MALE, 6, 8, 130); + saveRecommendation(carbohydrate, Gender.MALE, 9, 11, 130); + saveRecommendation(carbohydrate, Gender.MALE, 12, 14, 130); + saveRecommendation(carbohydrate, Gender.MALE, 15, 18, 130); + saveRecommendation(carbohydrate, Gender.MALE, 19, 29, 130); + saveRecommendation(carbohydrate, Gender.MALE, 30, 49, 130); + saveRecommendation(carbohydrate, Gender.MALE, 50, 64, 130); + saveRecommendation(carbohydrate, Gender.MALE, 65, 74, 130); + saveRecommendation(carbohydrate, Gender.MALE, 75, 200, 130); + + saveRecommendation(carbohydrate, Gender.FEMALE, 0, 0, 75); + saveRecommendation(carbohydrate, Gender.FEMALE, 1, 2, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 3, 5, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 6, 8, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 9, 11, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 12, 14, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 15, 18, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 19, 29, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 30, 49, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 50, 64, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 65, 74, 130); + saveRecommendation(carbohydrate, Gender.FEMALE, 75, 200, 130); + + + // 단백질 권장 섭취량 + saveRecommendation(protein, Gender.MALE, 0, 0, 12.5); + saveRecommendation(protein, Gender.MALE, 1, 2, 20); + saveRecommendation(protein, Gender.MALE, 3, 5, 25); + saveRecommendation(protein, Gender.MALE, 6, 8, 35); + saveRecommendation(protein, Gender.MALE, 9, 11, 50); + saveRecommendation(protein, Gender.MALE, 12, 14, 60); + saveRecommendation(protein, Gender.MALE, 15, 18, 65); + saveRecommendation(protein, Gender.MALE, 19, 29, 65); + saveRecommendation(protein, Gender.MALE, 30, 49, 65); + saveRecommendation(protein, Gender.MALE, 50, 64, 60); + saveRecommendation(protein, Gender.MALE, 65, 74, 60); + saveRecommendation(protein, Gender.MALE, 75, 200, 60); + + saveRecommendation(protein, Gender.FEMALE, 0, 0, 12.5); + saveRecommendation(protein, Gender.FEMALE, 1, 2, 20); + saveRecommendation(protein, Gender.FEMALE, 3, 5, 25); + saveRecommendation(protein, Gender.FEMALE, 6, 8, 35); + saveRecommendation(protein, Gender.FEMALE, 9, 11, 45); + saveRecommendation(protein, Gender.FEMALE, 12, 14, 55); + saveRecommendation(protein, Gender.FEMALE, 15, 18, 55); + saveRecommendation(protein, Gender.FEMALE, 19, 29, 55); + saveRecommendation(protein, Gender.FEMALE, 30, 49, 50); + saveRecommendation(protein, Gender.FEMALE, 50, 64, 50); + saveRecommendation(protein, Gender.FEMALE, 65, 74, 50); + saveRecommendation(protein, Gender.FEMALE, 75, 200, 50); + + + // 칼슘 권장 섭취량 + saveRecommendation(calcium, Gender.MALE, 0, 0, 275); + saveRecommendation(calcium, Gender.MALE, 1, 2, 500); + saveRecommendation(calcium, Gender.MALE, 3, 5, 600); + saveRecommendation(calcium, Gender.MALE, 6, 8, 700); + saveRecommendation(calcium, Gender.MALE, 9, 11, 800); + saveRecommendation(calcium, Gender.MALE, 12, 14, 1000); + saveRecommendation(calcium, Gender.MALE, 15, 18, 900); + saveRecommendation(calcium, Gender.MALE, 19, 29, 800); + saveRecommendation(calcium, Gender.MALE, 30, 49, 800); + saveRecommendation(calcium, Gender.MALE, 50, 64, 750); + saveRecommendation(calcium, Gender.MALE, 65, 74, 700); + saveRecommendation(calcium, Gender.MALE, 75, 200, 700); + + saveRecommendation(calcium, Gender.FEMALE, 0, 0, 275); + saveRecommendation(calcium, Gender.FEMALE, 1, 2, 500); + saveRecommendation(calcium, Gender.FEMALE, 3, 5, 600); + saveRecommendation(calcium, Gender.FEMALE, 6, 8, 700); + saveRecommendation(calcium, Gender.FEMALE, 9, 11, 800); + saveRecommendation(calcium, Gender.FEMALE, 12, 14, 900); + saveRecommendation(calcium, Gender.FEMALE, 15, 18, 800); + saveRecommendation(calcium, Gender.FEMALE, 19, 29, 700); + saveRecommendation(calcium, Gender.FEMALE, 30, 49, 700); + saveRecommendation(calcium, Gender.FEMALE, 50, 64, 800); + saveRecommendation(calcium, Gender.FEMALE, 65, 74, 800); + saveRecommendation(calcium, Gender.FEMALE, 75, 200, 800); + + + // 나트륨 권장 섭취량 + saveRecommendation(sodium, Gender.MALE, 0, 0, 240); + saveRecommendation(sodium, Gender.MALE, 1, 2, 810); + saveRecommendation(sodium, Gender.MALE, 3, 5, 1000); + saveRecommendation(sodium, Gender.MALE, 6, 8, 1200); + saveRecommendation(sodium, Gender.MALE, 9, 11, 1500); + saveRecommendation(sodium, Gender.MALE, 12, 14, 1500); + saveRecommendation(sodium, Gender.MALE, 15, 18, 1500); + saveRecommendation(sodium, Gender.MALE, 19, 29, 1500); + saveRecommendation(sodium, Gender.MALE, 30, 49, 1500); + saveRecommendation(sodium, Gender.MALE, 50, 64, 1500); + saveRecommendation(sodium, Gender.MALE, 65, 74, 1300); + saveRecommendation(sodium, Gender.MALE, 75, 200, 1100); + + saveRecommendation(sodium, Gender.FEMALE, 0, 0, 240); + saveRecommendation(sodium, Gender.FEMALE, 1, 2, 810); + saveRecommendation(sodium, Gender.FEMALE, 3, 5, 1000); + saveRecommendation(sodium, Gender.FEMALE, 6, 8, 1200); + saveRecommendation(sodium, Gender.FEMALE, 9, 11, 1500); + saveRecommendation(sodium, Gender.FEMALE, 12, 14, 1500); + saveRecommendation(sodium, Gender.FEMALE, 15, 18, 1500); + saveRecommendation(sodium, Gender.FEMALE, 19, 29, 1500); + saveRecommendation(sodium, Gender.FEMALE, 30, 49, 1500); + saveRecommendation(sodium, Gender.FEMALE, 50, 64, 1500); + saveRecommendation(sodium, Gender.FEMALE, 65, 74, 1300); + saveRecommendation(sodium, Gender.FEMALE, 75, 200, 1100); + + + // 비타민 E 권장 섭취량 + saveRecommendation(vitaminE, Gender.MALE, 0, 0, 3.5); + saveRecommendation(vitaminE, Gender.MALE, 1, 2, 5); + saveRecommendation(vitaminE, Gender.MALE, 3, 5, 6); + saveRecommendation(vitaminE, Gender.MALE, 6, 8, 7); + saveRecommendation(vitaminE, Gender.MALE, 9, 11, 9); + saveRecommendation(vitaminE, Gender.MALE, 12, 14, 11); + saveRecommendation(vitaminE, Gender.MALE, 15, 18, 12); + saveRecommendation(vitaminE, Gender.MALE, 19, 29, 12); + saveRecommendation(vitaminE, Gender.MALE, 30, 49, 12); + saveRecommendation(vitaminE, Gender.MALE, 50, 64, 12); + saveRecommendation(vitaminE, Gender.MALE, 65, 74, 12); + saveRecommendation(vitaminE, Gender.MALE, 75, 200, 12); + + saveRecommendation(vitaminE, Gender.FEMALE, 0, 0, 3.5); + saveRecommendation(vitaminE, Gender.FEMALE, 1, 2, 5); + saveRecommendation(vitaminE, Gender.FEMALE, 3, 5, 6); + saveRecommendation(vitaminE, Gender.FEMALE, 6, 8, 7); + saveRecommendation(vitaminE, Gender.FEMALE, 9, 11, 9); + saveRecommendation(vitaminE, Gender.FEMALE, 12, 14, 11); + saveRecommendation(vitaminE, Gender.FEMALE, 15, 18, 12); + saveRecommendation(vitaminE, Gender.FEMALE, 19, 29, 12); + saveRecommendation(vitaminE, Gender.FEMALE, 30, 49, 12); + saveRecommendation(vitaminE, Gender.FEMALE, 50, 64, 12); + saveRecommendation(vitaminE, Gender.FEMALE, 65, 74, 12); + saveRecommendation(vitaminE, Gender.FEMALE, 75, 200, 12); + + + // 비타민 B 권장 섭취량 + saveRecommendation(vitaminB, Gender.MALE, 0, 0, 0.2); + saveRecommendation(vitaminB, Gender.MALE, 1, 2, 20); + saveRecommendation(vitaminB, Gender.MALE, 3, 5, 30); + saveRecommendation(vitaminB, Gender.MALE, 6, 8, 45); + saveRecommendation(vitaminB, Gender.MALE, 9, 11, 60); + saveRecommendation(vitaminB, Gender.MALE, 12, 14, 80); + saveRecommendation(vitaminB, Gender.MALE, 15, 18, 95); + saveRecommendation(vitaminB, Gender.MALE, 19, 200, 100); + + saveRecommendation(vitaminB, Gender.FEMALE, 0, 0, 0.2); + saveRecommendation(vitaminB, Gender.FEMALE, 1, 2, 20); + saveRecommendation(vitaminB, Gender.FEMALE, 6, 8, 45); + saveRecommendation(vitaminB, Gender.FEMALE, 9, 11, 60); + saveRecommendation(vitaminB, Gender.FEMALE, 12, 14, 80); + saveRecommendation(vitaminB, Gender.FEMALE, 15, 18, 95); + saveRecommendation(vitaminB, Gender.FEMALE, 19, 200, 100); + + + // 지방 권장 섭취량 + saveRecommendation(fat, Gender.MALE, 1, 2, 27.5); + saveRecommendation(fat, Gender.MALE, 3, 5, 40); + saveRecommendation(fat, Gender.MALE, 6, 8, 47.5); + saveRecommendation(fat, Gender.MALE, 9, 11, 57.5); + saveRecommendation(fat, Gender.MALE, 12, 14, 72.5); + saveRecommendation(fat, Gender.MALE, 15, 18, 80.0); + saveRecommendation(fat, Gender.MALE, 19, 29, 72.5); + saveRecommendation(fat, Gender.MALE, 30, 49, 70.0); + saveRecommendation(fat, Gender.MALE, 50, 64, 62.5); + saveRecommendation(fat, Gender.MALE, 65, 74, 57.5); + saveRecommendation(fat, Gender.MALE, 75, 200, 55.0); + + saveRecommendation(fat, Gender.FEMALE, 1, 2, 27.5); + saveRecommendation(fat, Gender.FEMALE, 3, 5, 40); + saveRecommendation(fat, Gender.FEMALE, 6, 8, 42.5); + saveRecommendation(fat, Gender.FEMALE, 9, 11, 50.0); + saveRecommendation(fat, Gender.FEMALE, 12, 14, 57.5); + saveRecommendation(fat, Gender.FEMALE, 15, 18, 57.5); + saveRecommendation(fat, Gender.FEMALE, 19, 29, 57.5); + saveRecommendation(fat, Gender.FEMALE, 30, 49, 55.0); + saveRecommendation(fat, Gender.FEMALE, 50, 64, 50.0); + saveRecommendation(fat, Gender.FEMALE, 65, 74, 45.0); + saveRecommendation(fat, Gender.FEMALE, 75, 200, 42.5); + + + // 트랜스 지방 권장 섭취량 + saveRecommendation(transFat, Gender.MALE, 0, 2, 0); + saveRecommendation(transFat, Gender.MALE, 3, 5, 1.78); + saveRecommendation(transFat, Gender.MALE, 6, 8, 2.11); + saveRecommendation(transFat, Gender.MALE, 9, 11, 2.56); + saveRecommendation(transFat, Gender.MALE, 12, 14, 3.22); + saveRecommendation(transFat, Gender.MALE, 15, 18, 3.56); + saveRecommendation(transFat, Gender.MALE, 19, 29, 3.22); + saveRecommendation(transFat, Gender.MALE, 30, 49, 3.11); + saveRecommendation(transFat, Gender.MALE, 50, 64, 2.78); + saveRecommendation(transFat, Gender.MALE, 65, 74, 2.56); + saveRecommendation(transFat, Gender.MALE, 75, 200, 2.44); + + saveRecommendation(transFat, Gender.FEMALE, 0, 2, 0); + saveRecommendation(transFat, Gender.FEMALE, 3, 5, 1.78); + saveRecommendation(transFat, Gender.FEMALE, 6, 8, 1.89); + saveRecommendation(transFat, Gender.FEMALE, 9, 11, 2.22); + saveRecommendation(transFat, Gender.FEMALE, 12, 14, 2.56); + saveRecommendation(transFat, Gender.FEMALE, 15, 18, 2.56); + saveRecommendation(transFat, Gender.FEMALE, 19, 29, 2.56); + saveRecommendation(transFat, Gender.FEMALE, 30, 49, 2.44); + saveRecommendation(transFat, Gender.FEMALE, 50, 64, 2.22); + saveRecommendation(transFat, Gender.FEMALE, 65, 74, 2.00); + saveRecommendation(transFat, Gender.FEMALE, 75, 200, 1.89); + + + // 포화 지방 권장 섭취량 + saveRecommendation(saturatedFat, Gender.MALE, 0, 2, 0); + saveRecommendation(saturatedFat, Gender.MALE, 3, 5, 14.22); + saveRecommendation(saturatedFat, Gender.MALE, 6, 8, 16.89); + saveRecommendation(saturatedFat, Gender.MALE, 9, 11, 20.44); + saveRecommendation(saturatedFat, Gender.MALE, 12, 14, 25.78); + saveRecommendation(saturatedFat, Gender.MALE, 15, 18, 28.44); + saveRecommendation(saturatedFat, Gender.MALE, 19, 29, 22.56); + saveRecommendation(saturatedFat, Gender.MALE, 30, 49, 21.78); + saveRecommendation(saturatedFat, Gender.MALE, 50, 64, 19.44); + saveRecommendation(saturatedFat, Gender.MALE, 65, 74, 17.89); + saveRecommendation(saturatedFat, Gender.MALE, 75, 200, 17.11); + + saveRecommendation(saturatedFat, Gender.FEMALE, 0, 2, 0); + saveRecommendation(saturatedFat, Gender.FEMALE, 3, 5, 14.22); + saveRecommendation(saturatedFat, Gender.FEMALE, 6, 8, 15.11); + saveRecommendation(saturatedFat, Gender.FEMALE, 9, 11, 17.78); + saveRecommendation(saturatedFat, Gender.FEMALE, 12, 14, 20.44); + saveRecommendation(saturatedFat, Gender.FEMALE, 15, 18, 20.44); + saveRecommendation(saturatedFat, Gender.FEMALE, 19, 29, 17.89); + saveRecommendation(saturatedFat, Gender.FEMALE, 30, 49, 17.11); + saveRecommendation(saturatedFat, Gender.FEMALE, 50, 64, 15.56); + saveRecommendation(saturatedFat, Gender.FEMALE, 65, 74, 14.00); + saveRecommendation(saturatedFat, Gender.FEMALE, 75, 200, 13.22); + + } + + private void saveRecommendation(Nutrient nutrient, Gender gender, int minAge, int maxAge, double amount) { + NutrientRecommendation recommendation = NutrientRecommendation.builder() + .nutrient(nutrient) + .gender(gender) + .minAge(minAge) + .maxAge(maxAge) + .amount(amount) + .build(); + + recommendationRepository.save(recommendation); + } +} \ No newline at end of file From 02a1eb9f2bd6e3ad3b316b99cf44417784c8ba79 Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 7 May 2025 03:03:10 +0900 Subject: [PATCH 119/309] =?UTF-8?q?#31=20fix:=20enum=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=97=AC=EB=9F=AC=20=EA=B0=92=20=EB=B0=9B=EC=9D=84=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/healthfood/domain/HealthFood.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java index dc52832..cf34ae2 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java @@ -8,6 +8,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.HashSet; +import java.util.Set; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -24,12 +27,17 @@ public class HealthFood extends BaseEntity { @Column(nullable = false, columnDefinition = "TEXT") private String functionality; - @Column + @ElementCollection(targetClass = Keyword.class) + @CollectionTable( + name = "health_food_keywords", + joinColumns = @JoinColumn(name = "health_food_id") + ) @Enumerated(EnumType.STRING) - private Keyword keywords; + @Column(name = "keywords") + private Set keywords = new HashSet<>(); @Builder - public HealthFood(String itemName, String functionality, Keyword keywords) { + public HealthFood(String itemName, String functionality, Set keywords) { this.itemName = itemName; this.functionality = functionality; this.keywords = keywords; From db76659fc159452c65bc1e9f3b78a9c62abed471 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 8 May 2025 03:20:44 +0900 Subject: [PATCH 120/309] =?UTF-8?q?#31=20fix:=20healthFood,=20healthFoodKe?= =?UTF-8?q?yword,=20Keyword=EC=9D=98=20=EB=8B=A4=EB=8C=80=EB=8B=A4=20?= =?UTF-8?q?=EA=B4=80=EA=B3=84=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HealthFoodService.java | 2 + .../backend/healthfood/domain/HealthFood.java | 23 +++++------ .../healthfood/domain/HealthFoodKeyword.java | 40 +++++++++++++++++++ .../backend/healthfood/domain/Keyword.java | 38 ++++++++++++++++++ 4 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java create mode 100644 src/main/java/com/webeye/backend/healthfood/domain/Keyword.java diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 5b9c1e3..91ca6a1 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -35,6 +35,8 @@ public HealthFoodResponse.I2710 callHealthFoodApi() { List healthFoods = HealthFoodMapper.toEntityList(rows); + healthFoodRepository.saveAll(healthFoods); + return HealthFoodMapper.toResponseList(healthFoods, i2710.totalCount()); } } diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java index cf34ae2..2683407 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java @@ -1,15 +1,14 @@ package com.webeye.backend.healthfood.domain; import com.webeye.backend.global.domain.BaseEntity; -import com.webeye.backend.healthfood.domain.type.Keyword; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -27,19 +26,17 @@ public class HealthFood extends BaseEntity { @Column(nullable = false, columnDefinition = "TEXT") private String functionality; - @ElementCollection(targetClass = Keyword.class) - @CollectionTable( - name = "health_food_keywords", - joinColumns = @JoinColumn(name = "health_food_id") - ) - @Enumerated(EnumType.STRING) - @Column(name = "keywords") - private Set keywords = new HashSet<>(); + @OneToMany(mappedBy = "healthFood", cascade = CascadeType.ALL) + private List keyword = new ArrayList<>(); @Builder - public HealthFood(String itemName, String functionality, Set keywords) { + public HealthFood(String itemName, String functionality) { this.itemName = itemName; this.functionality = functionality; - this.keywords = keywords; + } + + public void addKeyword(HealthFoodKeyword healthFoodKeyword) { + keyword.add(healthFoodKeyword); + healthFoodKeyword.associateWithHealthFood(this); } } diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java new file mode 100644 index 0000000..ce91197 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java @@ -0,0 +1,40 @@ +package com.webeye.backend.healthfood.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HealthFoodKeyword { + + @Id + @Column(name = "health_food_keyword_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "health_food_id") + private HealthFood healthFood; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "keyword_id") + private Keyword keyword; + + @Builder + public HealthFoodKeyword(HealthFood healthFood, Keyword keyword) { + this.healthFood = healthFood; + this.keyword = keyword; + } + + public void associateWithHealthFood(HealthFood healthFood) { + this.healthFood = healthFood; + } + + public void associateWithKeyword(Keyword keyword) { + this.keyword = keyword; + } +} diff --git a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java new file mode 100644 index 0000000..161abe9 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java @@ -0,0 +1,38 @@ +package com.webeye.backend.healthfood.domain; + +import com.webeye.backend.global.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Keyword extends BaseEntity { + + @Id + @Column(name = "keyword_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String keyword; + + @OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL) + private List healthFood = new ArrayList<>(); + + @Builder + public Keyword(String keyword) { + this.keyword = keyword; + } + + public void addHealthFood(HealthFoodKeyword healthFoodKeyword) { + healthFood.add(healthFoodKeyword); + healthFoodKeyword.associateWithKeyword(this); + } +} From 743d9e824201d6e5199e9451e45b077f30d4cd40 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 8 May 2025 21:38:38 +0900 Subject: [PATCH 121/309] #32 feat: extends BaseEntity --- .../backend/nutrition/domain/NutrientRecommendation.java | 3 ++- src/main/java/com/webeye/backend/product/domain/Product.java | 5 +++-- .../com/webeye/backend/product/domain/ProductAllergy.java | 3 ++- .../com/webeye/backend/product/domain/ProductNutrient.java | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/webeye/backend/nutrition/domain/NutrientRecommendation.java b/src/main/java/com/webeye/backend/nutrition/domain/NutrientRecommendation.java index 8e7cadd..ecdc5ea 100644 --- a/src/main/java/com/webeye/backend/nutrition/domain/NutrientRecommendation.java +++ b/src/main/java/com/webeye/backend/nutrition/domain/NutrientRecommendation.java @@ -1,5 +1,6 @@ package com.webeye.backend.nutrition.domain; +import com.webeye.backend.global.domain.BaseEntity; import com.webeye.backend.nutrition.domain.type.Gender; import jakarta.persistence.*; import lombok.AccessLevel; @@ -10,7 +11,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class NutrientRecommendation { +public class NutrientRecommendation extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 0efa674..a473901 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -1,5 +1,6 @@ package com.webeye.backend.product.domain; +import com.webeye.backend.global.domain.BaseEntity; import jakarta.persistence.*; import lombok.*; @@ -9,9 +10,9 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Product { +public class Product extends BaseEntity { @Id - @Column(name = "product_id") + @Column(name = "product_id", nullable = false) private String id; // 쿠팡에서 products 뒤에 오는 숫자 @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java b/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java index ba7bdbf..fd36e51 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java +++ b/src/main/java/com/webeye/backend/product/domain/ProductAllergy.java @@ -1,6 +1,7 @@ package com.webeye.backend.product.domain; import com.webeye.backend.allergy.type.AllergyType; +import com.webeye.backend.global.domain.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -10,7 +11,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ProductAllergy { +public class ProductAllergy extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java index 86cf3e6..dbe2788 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java +++ b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java @@ -1,5 +1,6 @@ package com.webeye.backend.product.domain; +import com.webeye.backend.global.domain.BaseEntity; import com.webeye.backend.nutrition.domain.Nutrient; import jakarta.persistence.*; import lombok.*; @@ -7,7 +8,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ProductNutrient { +public class ProductNutrient extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; From 8302c0e48169668a5423f4924d93b66bd8943906 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 8 May 2025 21:39:56 +0900 Subject: [PATCH 122/309] =?UTF-8?q?#32=20feat:=20nutrient,=20gender,=20age?= =?UTF-8?q?=EB=A1=9C=20=EA=B6=8C=EC=9E=A5=EB=9F=89=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NutrientRecommendationRepository.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRecommendationRepository.java b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRecommendationRepository.java index 76758dc..891eceb 100644 --- a/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRecommendationRepository.java +++ b/src/main/java/com/webeye/backend/nutrition/persistent/NutrientRecommendationRepository.java @@ -1,9 +1,24 @@ package com.webeye.backend.nutrition.persistent; +import com.webeye.backend.nutrition.domain.Nutrient; import com.webeye.backend.nutrition.domain.NutrientRecommendation; +import com.webeye.backend.nutrition.domain.type.Gender; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface NutrientRecommendationRepository extends JpaRepository { + + @Query("SELECT r FROM NutrientRecommendation r " + + "WHERE r.nutrient.id = :nutrientId " + + "AND r.gender = :gender " + + "AND :age BETWEEN r.minAge AND r.maxAge") + Optional findByNutrientIdAndGenderAndAge( + @Param("nutrientId") Long nutrientId, + @Param("gender") Gender gender, + @Param("age") int age); } From fc1b14333eb1feac54b03057ecea3c2595c2c2c4 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 8 May 2025 21:41:44 +0900 Subject: [PATCH 123/309] =?UTF-8?q?#32=20feat:=20=EC=98=81=EC=96=91?= =?UTF-8?q?=EC=84=B1=EB=B6=84=20=EA=B6=8C=EC=9E=A5=EB=9F=89=EC=9D=98=2040%?= =?UTF-8?q?=20=EB=84=98=EB=8A=94=20=EC=8B=9D=ED=92=88=EB=A7=8C=20=EB=B0=98?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NutrientRecommendationService.java | 51 +++++++++++++++++++ .../NutrientRecommendationRequest.java | 13 +++++ .../NutrientRecommendationResponse.java | 16 ++++++ 3 files changed, 80 insertions(+) create mode 100644 src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java create mode 100644 src/main/java/com/webeye/backend/nutrition/dto/request/NutrientRecommendationRequest.java create mode 100644 src/main/java/com/webeye/backend/nutrition/dto/response/NutrientRecommendationResponse.java diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java b/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java new file mode 100644 index 0000000..16c83db --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java @@ -0,0 +1,51 @@ +package com.webeye.backend.nutrition.application; + +import com.webeye.backend.nutrition.domain.Nutrient; +import com.webeye.backend.nutrition.domain.NutrientRecommendation; +import com.webeye.backend.nutrition.dto.request.NutrientRecommendationRequest; +import com.webeye.backend.nutrition.dto.response.NutrientRecommendationResponse; +import com.webeye.backend.nutrition.persistent.NutrientRecommendationRepository; +import com.webeye.backend.product.domain.ProductNutrient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Year; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class NutrientRecommendationService { + private final NutrientRecommendationRepository recommendationRepository; + + @Transactional + public List analyzeNutrientSufficiency(NutrientRecommendationRequest request) { + List overRecommendationNutrients = new LinkedList<>(); + int userAge = Year.now().getValue() - request.birthYear(); + + for (ProductNutrient productNutrient : request.product().getNutrients()) { + Nutrient nutrient = productNutrient.getNutrient(); + + Optional recommendationOpt = recommendationRepository + .findByNutrientIdAndGenderAndAge(nutrient.getId(), request.gender(), userAge); + + if (recommendationOpt.isEmpty()) { + continue; + } + + NutrientRecommendation recommendation = recommendationOpt.get(); + double percentage = productNutrient.getAmount() / recommendation.getAmount() * 100; + + if (percentage >= 40.0) { + overRecommendationNutrients.add(NutrientRecommendationResponse.builder() + .nutrientType(productNutrient.getNutrient().getType()) + .percnetage((int) percentage).build()); + } + } + return overRecommendationNutrients; + } +} diff --git a/src/main/java/com/webeye/backend/nutrition/dto/request/NutrientRecommendationRequest.java b/src/main/java/com/webeye/backend/nutrition/dto/request/NutrientRecommendationRequest.java new file mode 100644 index 0000000..c9181da --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/dto/request/NutrientRecommendationRequest.java @@ -0,0 +1,13 @@ +package com.webeye.backend.nutrition.dto.request; + +import com.webeye.backend.nutrition.domain.type.Gender; +import com.webeye.backend.product.domain.Product; +import lombok.Builder; + +@Builder +public record NutrientRecommendationRequest( + Product product, + int birthYear, + Gender gender +) { +} diff --git a/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientRecommendationResponse.java b/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientRecommendationResponse.java new file mode 100644 index 0000000..bd83284 --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientRecommendationResponse.java @@ -0,0 +1,16 @@ +package com.webeye.backend.nutrition.dto.response; + +import com.webeye.backend.nutrition.domain.type.NutrientType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema(description = "영양성분 권장량에 대한 비율 응답") +@Builder +public record NutrientRecommendationResponse( + @Schema(description = "영양성분 이름") + NutrientType nutrientType, + + @Schema(description = "권장량에 대한 영양성분의 비율") + int percnetage +) { +} From 71405d5f6c7f3ff55bfd0fb2add684e798c05163 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 8 May 2025 21:45:57 +0900 Subject: [PATCH 124/309] =?UTF-8?q?#32=20feat:=20=EC=8B=9D=ED=92=88=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EA=B8=B0=EB=8A=A5=20-=20=EC=98=81?= =?UTF-8?q?=EC=96=91=EC=84=B1=EB=B6=84=20=EB=B6=84=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/application/ProductService.java | 24 ++++++++++++++----- .../dto/request/ProductAnalysisRequest.java | 7 ++++++ .../product/dto/response/ProductResponse.java | 9 +++---- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 1fbe128..8827439 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -4,6 +4,9 @@ import com.webeye.backend.allergy.type.AllergyType; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; +import com.webeye.backend.nutrition.application.NutrientRecommendationService; +import com.webeye.backend.nutrition.dto.request.NutrientRecommendationRequest; +import com.webeye.backend.nutrition.dto.response.NutrientRecommendationResponse; import com.webeye.backend.product.domain.ProductAllergy; import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.nutrition.application.NutritionService; @@ -21,16 +24,20 @@ @Transactional(readOnly = true) public class ProductService { private final NutritionService nutritionService; + private final NutrientRecommendationService nutrientRecommendationService; private final AllergyService allergyService; private final ProductRepository productRepository; - // TODO: 음식 제품 영양성분 분석 @Transactional public ProductResponse analyzeFoodProduct(ProductAnalysisRequest request) { if (productRepository.existsById(request.productId())) { + Product product = productRepository.findById(request.productId()) + .orElseThrow(() -> new BusinessException(ErrorCode.PRODUCT_NOT_FOUND)); + return ProductResponse.builder() - .allergyTypes(getAllergyResponse(request.productId(), request.allergies())) + .allergyTypes(getAllergyResponse(product, request.allergies())) + .overRecommendationNutrients(getNutrientRecommendationResponse(request, product)) .build(); } @@ -43,17 +50,22 @@ public ProductResponse analyzeFoodProduct(ProductAnalysisRequest request) { allergyService.saveProductAllergy(product, request); return ProductResponse.builder() - .allergyTypes(getAllergyResponse(request.productId(), request.allergies())) + .allergyTypes(getAllergyResponse(product, request.allergies())) + .overRecommendationNutrients(getNutrientRecommendationResponse(request, product)) .build(); } - private List getAllergyResponse(String productId, List userAllergies) { - Product product = productRepository.findById(productId).orElseThrow(() -> new BusinessException(ErrorCode.PRODUCT_NOT_FOUND)); - + private List getAllergyResponse(Product product, List userAllergies) { return product.getAllergies().stream() .map(ProductAllergy::getAllergy) .distinct() .filter(userAllergies::contains) .toList(); } + + private List getNutrientRecommendationResponse(ProductAnalysisRequest request, Product product) { + return nutrientRecommendationService.analyzeNutrientSufficiency(NutrientRecommendationRequest + .builder().birthYear(request.birthYear()).gender(request.gender()).product(product).build()); + } + } diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java index 092cc6a..a214c58 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java @@ -1,6 +1,7 @@ package com.webeye.backend.product.dto.request; import com.webeye.backend.allergy.type.AllergyType; +import com.webeye.backend.nutrition.domain.type.Gender; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Builder; @@ -16,6 +17,12 @@ public record ProductAnalysisRequest( @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") List urls, + @Schema(description = "사용자 출생년도") + int birthYear, + + @Schema(description = "사용자 성별") + Gender gender, + @Schema(description = "사용자 알레르기") List allergies ) { diff --git a/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java index d813444..304bb88 100644 --- a/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java +++ b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java @@ -2,6 +2,7 @@ import com.webeye.backend.allergy.type.AllergyType; import com.webeye.backend.nutrition.domain.type.NutrientType; +import com.webeye.backend.nutrition.dto.response.NutrientRecommendationResponse; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; @@ -10,10 +11,10 @@ @Schema(description = "제품 분석 응답 (SODIUM: 나트륨, CARBOHYDRATE: 탄수화물, SUGARS: 당류, FAT: 지방, TRANS_FAT: 트랜스지방, SATURATED_FAT: 포화지방, CHOLESTEROL: 콜레스테롤, PROTEIN: 단백질, CALCIUM: 칼슘, PHOSPHORUS: 인, NIACIN: 나이아신, VITAMIN_B: 비타민 B, VITAMIN_E: 비타민 E)") @Builder public record ProductResponse ( - @Schema(description = "포함된 주의 영양성분") - List nutrientTypes, - @Schema(description = "포함된 알레르기 유발 성분") - List allergyTypes + List allergyTypes, + + @Schema(description = "영양성분 권장량을 넘는 성분") + List overRecommendationNutrients ) { } From 58bdb5481bbb6883b1d420cdfa9b49dadaad2878 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 8 May 2025 22:38:27 +0900 Subject: [PATCH 125/309] =?UTF-8?q?#32=20fix:=20percentage=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nutrition/application/NutrientRecommendationService.java | 2 +- .../nutrition/dto/response/NutrientRecommendationResponse.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java b/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java index 16c83db..9bd451a 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java @@ -43,7 +43,7 @@ public List analyzeNutrientSufficiency(NutrientR if (percentage >= 40.0) { overRecommendationNutrients.add(NutrientRecommendationResponse.builder() .nutrientType(productNutrient.getNutrient().getType()) - .percnetage((int) percentage).build()); + .percentage((int) percentage).build()); } } return overRecommendationNutrients; diff --git a/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientRecommendationResponse.java b/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientRecommendationResponse.java index bd83284..cabe5d9 100644 --- a/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientRecommendationResponse.java +++ b/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientRecommendationResponse.java @@ -11,6 +11,6 @@ public record NutrientRecommendationResponse( NutrientType nutrientType, @Schema(description = "권장량에 대한 영양성분의 비율") - int percnetage + int percentage ) { } From 8df2b3ca00bb9f2cdeb3eb81d98da6864b2aa9fd Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 8 May 2025 22:48:12 +0900 Subject: [PATCH 126/309] =?UTF-8?q?#32=20fix:=20=EA=B6=8C=EC=9E=A5?= =?UTF-8?q?=EB=9F=89=EC=9D=B4=200=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/NutrientRecommendationService.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java b/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java index 9bd451a..c04ab87 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java @@ -20,6 +20,8 @@ @Service @RequiredArgsConstructor public class NutrientRecommendationService { + private final int ZERO_RECOMMENDATION = 100; + private final NutrientRecommendationRepository recommendationRepository; @Transactional @@ -38,7 +40,7 @@ public List analyzeNutrientSufficiency(NutrientR } NutrientRecommendation recommendation = recommendationOpt.get(); - double percentage = productNutrient.getAmount() / recommendation.getAmount() * 100; + double percentage = getPercentageOfRecommendation(recommendation.getAmount(), productNutrient.getAmount()); if (percentage >= 40.0) { overRecommendationNutrients.add(NutrientRecommendationResponse.builder() @@ -48,4 +50,11 @@ public List analyzeNutrientSufficiency(NutrientR } return overRecommendationNutrients; } + + private double getPercentageOfRecommendation(double recommendationAmount, double productAmount) { + if (recommendationAmount > 0) { + return productAmount / recommendationAmount * 100; + } + return ZERO_RECOMMENDATION; + } } From 60d73292ad9bf483e176cc49b9d83de3e8ed4086 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 8 May 2025 22:49:14 +0900 Subject: [PATCH 127/309] =?UTF-8?q?#32=20feat:=20=EB=B9=84=ED=83=80?= =?UTF-8?q?=EB=AF=BC=20B=203-5=EC=84=B8=20=EC=97=AC=EC=84=B1=20=EA=B6=8C?= =?UTF-8?q?=EC=9E=A5=EB=9F=89=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nutrition/persistent/init/NutrientRecommendationInit.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientRecommendationInit.java b/src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientRecommendationInit.java index c582e1c..8279b9c 100644 --- a/src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientRecommendationInit.java +++ b/src/main/java/com/webeye/backend/nutrition/persistent/init/NutrientRecommendationInit.java @@ -201,6 +201,7 @@ public void run(ApplicationArguments args) { saveRecommendation(vitaminB, Gender.FEMALE, 0, 0, 0.2); saveRecommendation(vitaminB, Gender.FEMALE, 1, 2, 20); + saveRecommendation(vitaminB, Gender.FEMALE, 3, 5, 30); saveRecommendation(vitaminB, Gender.FEMALE, 6, 8, 45); saveRecommendation(vitaminB, Gender.FEMALE, 9, 11, 60); saveRecommendation(vitaminB, Gender.FEMALE, 12, 14, 80); From ea67c25024ac9ff58bb00dedc0bf761c684afae3 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 8 May 2025 22:50:27 +0900 Subject: [PATCH 128/309] =?UTF-8?q?#32=20docs:=20=EC=98=81=EC=96=91?= =?UTF-8?q?=EC=84=B1=EB=B6=84=20=EB=B6=84=EC=84=9D=20=EC=84=A4=EB=AA=85=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 --- .../backend/product/presentation/swagger/ProductSwagger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java index b50f4d4..baabbaf 100644 --- a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -13,7 +13,7 @@ public interface ProductSwagger { @Operation( summary = "음식 제품 분석", - description = "음식 제품을 분석합니다. 질환, 알레르기를 입력받아 이를 유발하는 성분이 포함되어 있는지를 반환합니다." + description = "음식 제품을 분석합니다. 질환, 알레르기를 입력받아 이를 유발하는 성분과 영양소 권장량을 초과하는 성분이 포함되어 있는지를 반환합니다." ) @ApiResponses(value = { @ApiResponse( From 1175f17d3df6a9b17fcee7b58576c90602ce3c16 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 8 May 2025 22:53:47 +0900 Subject: [PATCH 129/309] =?UTF-8?q?#32=20feat:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EB=B9=84=EC=96=B4=EC=9E=88?= =?UTF-8?q?=EB=8A=94=EC=A7=80=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/product/dto/request/ProductAnalysisRequest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java index a214c58..4554677 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java @@ -14,13 +14,16 @@ public record ProductAnalysisRequest( @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") String productId, + @Schema(description = "상품 이미지 URL") @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") List urls, @Schema(description = "사용자 출생년도") + @NotEmpty(message = "사용자의 출생연도는 비어있을 수 없습니다.") int birthYear, @Schema(description = "사용자 성별") + @NotEmpty(message = "사용자의 성별은 비어있을 수 없습니다.") Gender gender, @Schema(description = "사용자 알레르기") From f39dee03b47fe7aad38efb8126f08f98e0c9da75 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 9 May 2025 06:06:20 +0900 Subject: [PATCH 130/309] =?UTF-8?q?#31=20refactor:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/healthfood/domain/HealthFood.java | 4 ++-- .../java/com/webeye/backend/healthfood/domain/Keyword.java | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java index 2683407..f06d636 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java @@ -27,7 +27,7 @@ public class HealthFood extends BaseEntity { private String functionality; @OneToMany(mappedBy = "healthFood", cascade = CascadeType.ALL) - private List keyword = new ArrayList<>(); + private List healthFoodKeywords= new ArrayList<>(); @Builder public HealthFood(String itemName, String functionality) { @@ -36,7 +36,7 @@ public HealthFood(String itemName, String functionality) { } public void addKeyword(HealthFoodKeyword healthFoodKeyword) { - keyword.add(healthFoodKeyword); + healthFoodKeywords.add(healthFoodKeyword); healthFoodKeyword.associateWithHealthFood(this); } } diff --git a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java index 161abe9..b54b83a 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java @@ -1,6 +1,5 @@ package com.webeye.backend.healthfood.domain; -import com.webeye.backend.global.domain.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -13,7 +12,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Keyword extends BaseEntity { +public class Keyword { @Id @Column(name = "keyword_id", nullable = false) @@ -24,7 +23,7 @@ public class Keyword extends BaseEntity { private String keyword; @OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL) - private List healthFood = new ArrayList<>(); + private List healthFoodKeywords = new ArrayList<>(); @Builder public Keyword(String keyword) { @@ -32,7 +31,7 @@ public Keyword(String keyword) { } public void addHealthFood(HealthFoodKeyword healthFoodKeyword) { - healthFood.add(healthFoodKeyword); + healthFoodKeywords.add(healthFoodKeyword); healthFoodKeyword.associateWithKeyword(this); } } From 74484ab9509651791678ad9872cc05135dd880a1 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 9 May 2025 06:16:10 +0900 Subject: [PATCH 131/309] =?UTF-8?q?#31=20refactor:=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=ED=83=80=EC=9E=85=20enum=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/healthfood/domain/Keyword.java | 8 +++++--- .../domain/type/{Keyword.java => HealthFoodType.java} | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) rename src/main/java/com/webeye/backend/healthfood/domain/type/{Keyword.java => HealthFoodType.java} (94%) diff --git a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java index b54b83a..b4e67b3 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java @@ -1,5 +1,6 @@ package com.webeye.backend.healthfood.domain; +import com.webeye.backend.healthfood.domain.type.HealthFoodType; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -19,15 +20,16 @@ public class Keyword { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Enumerated(EnumType.STRING) @Column(nullable = false, unique = true) - private String keyword; + private HealthFoodType type; @OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL) private List healthFoodKeywords = new ArrayList<>(); @Builder - public Keyword(String keyword) { - this.keyword = keyword; + public Keyword(HealthFoodType type) { + this.type = type; } public void addHealthFood(HealthFoodKeyword healthFoodKeyword) { diff --git a/src/main/java/com/webeye/backend/healthfood/domain/type/Keyword.java b/src/main/java/com/webeye/backend/healthfood/domain/type/HealthFoodType.java similarity index 94% rename from src/main/java/com/webeye/backend/healthfood/domain/type/Keyword.java rename to src/main/java/com/webeye/backend/healthfood/domain/type/HealthFoodType.java index c860f91..b5f29fa 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/type/Keyword.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/type/HealthFoodType.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public enum Keyword { +public enum HealthFoodType { IMMUNE("면역기능"), SKIN("피부건강"), BLOOD("혈액건강"), @@ -39,7 +39,7 @@ public enum Keyword { private final String description; - Keyword(String description) { + HealthFoodType(String description) { this.description = description; } } From fb7de12e16d6a2c2fa8ded9a00cc11c614024897 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 9 May 2025 06:37:27 +0900 Subject: [PATCH 132/309] =?UTF-8?q?#31=20feat:=20repository=20=EB=B0=8F=20?= =?UTF-8?q?healthFoodInit=20keyword=20=EC=9E=90=EB=8F=99=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HealthFoodKeywordRepository.java | 7 ++++ .../persistence/KeywordRepository.java | 11 +++++++ .../persistence/init/HealthFoodInit.java | 33 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodKeywordRepository.java create mode 100644 src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/KeywordRepository.java create mode 100644 src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/init/HealthFoodInit.java diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodKeywordRepository.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodKeywordRepository.java new file mode 100644 index 0000000..48d807d --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodKeywordRepository.java @@ -0,0 +1,7 @@ +package com.webeye.backend.healthfood.infrastructure.persistence; + +import com.webeye.backend.healthfood.domain.HealthFoodKeyword; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface HealthFoodKeywordRepository extends JpaRepository { +} diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/KeywordRepository.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/KeywordRepository.java new file mode 100644 index 0000000..26cb105 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/KeywordRepository.java @@ -0,0 +1,11 @@ +package com.webeye.backend.healthfood.infrastructure.persistence; + +import com.webeye.backend.healthfood.domain.Keyword; +import com.webeye.backend.healthfood.domain.type.HealthFoodType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface KeywordRepository extends JpaRepository { + Optional findByType(HealthFoodType type); +} diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/init/HealthFoodInit.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/init/HealthFoodInit.java new file mode 100644 index 0000000..67b007d --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/init/HealthFoodInit.java @@ -0,0 +1,33 @@ +package com.webeye.backend.healthfood.infrastructure.persistence.init; + +import com.webeye.backend.global.util.DummyDataInit; +import com.webeye.backend.healthfood.domain.Keyword; +import com.webeye.backend.healthfood.domain.type.HealthFoodType; +import com.webeye.backend.healthfood.infrastructure.persistence.KeywordRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; + +@Slf4j +@RequiredArgsConstructor +@Order(3) +@DummyDataInit +public class HealthFoodInit implements ApplicationRunner { + + private final KeywordRepository keywordRepository; + + @Override + public void run(ApplicationArguments args) { + for (HealthFoodType healthFoodType : HealthFoodType.values()) { + if (keywordRepository.findByType(healthFoodType).isEmpty()) { + Keyword keyword = Keyword.builder() + .type(healthFoodType) + .build(); + + keywordRepository.save(keyword); + } + } + } +} From fe646bbb3080e89d4272ce877a9be4c78d108579 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 9 May 2025 06:38:50 +0900 Subject: [PATCH 133/309] =?UTF-8?q?#31=20feat:=20baseEntity=20=EC=83=81?= =?UTF-8?q?=EC=86=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/healthfood/domain/HealthFoodKeyword.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java index ce91197..72adee2 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java @@ -1,5 +1,6 @@ package com.webeye.backend.healthfood.domain; +import com.webeye.backend.global.domain.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -9,7 +10,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class HealthFoodKeyword { +public class HealthFoodKeyword extends BaseEntity { @Id @Column(name = "health_food_keyword_id", nullable = false) From 4c0eca5691c7ba1996f8be78d68d3cb142ab39b9 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sun, 11 May 2025 18:05:47 +0900 Subject: [PATCH 134/309] #32 refactor: LinkedList -> ArrayList --- .../application/NutrientRecommendationService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java b/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java index c04ab87..c78054a 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutrientRecommendationService.java @@ -12,7 +12,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.Year; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -20,13 +20,13 @@ @Service @RequiredArgsConstructor public class NutrientRecommendationService { - private final int ZERO_RECOMMENDATION = 100; + private static final int ZERO_RECOMMENDATION = 100; private final NutrientRecommendationRepository recommendationRepository; @Transactional public List analyzeNutrientSufficiency(NutrientRecommendationRequest request) { - List overRecommendationNutrients = new LinkedList<>(); + List overRecommendationNutrients = new ArrayList<>(); int userAge = Year.now().getValue() - request.birthYear(); for (ProductNutrient productNutrient : request.product().getNutrients()) { From 010bc86d9c0ac65edfaaab384754e84e79f8566f Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 11 May 2025 21:21:49 +0900 Subject: [PATCH 135/309] =?UTF-8?q?#31=20fix:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81=E3=85=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/persistence/init/HealthFoodInit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/init/HealthFoodInit.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/init/HealthFoodInit.java index 67b007d..2c22497 100644 --- a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/init/HealthFoodInit.java +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/init/HealthFoodInit.java @@ -12,7 +12,7 @@ @Slf4j @RequiredArgsConstructor -@Order(3) +@Order(1) @DummyDataInit public class HealthFoodInit implements ApplicationRunner { From 2bd0106406716ed8e4d209a4ae8dbfdd3a517068 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 11 May 2025 22:55:40 +0900 Subject: [PATCH 136/309] =?UTF-8?q?#33=20feat:=20=EA=B1=B4=EA=B0=95?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=8B=9D=ED=92=88=20response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../healthfood/dto/HealthFoodAiResponse.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java diff --git a/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java new file mode 100644 index 0000000..12f229b --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java @@ -0,0 +1,84 @@ +package com.webeye.backend.healthfood.dto; + +import com.webeye.backend.healthfood.domain.type.HealthFoodType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.util.ArrayList; +import java.util.List; + +@Builder +@Schema(description = "건강 기능 식품 분석") +public record HealthFoodAiResponse( + boolean immune, + boolean skin, + boolean blood, + boolean bodyFat, + boolean bloodSugar, + boolean memory, + boolean antioxidant, + boolean gut, + boolean liver, + boolean eye, + boolean joint, + boolean sleep, + boolean stressFatigue, + boolean menopause, + boolean prostate, + boolean urinary, + boolean energy, + boolean bone, + boolean muscle, + boolean cognition, + boolean stomach, + boolean oral, + boolean hair, + boolean growth, + boolean bloodPressure, + boolean urination, + boolean folate, + boolean nose, + boolean maleHealth, + boolean electrolyte, + boolean dietaryFiber, + boolean essentialFattyAcid +) { + public List getHealthFoodTypes() { + List healthFoodTypes = new ArrayList<>(); + + if (immune) healthFoodTypes.add(HealthFoodType.IMMUNE); + if (skin) healthFoodTypes.add(HealthFoodType.SKIN); + if (blood) healthFoodTypes.add(HealthFoodType.BLOOD); + if (bodyFat) healthFoodTypes.add(HealthFoodType.BODY_FAT); + if (bloodPressure) healthFoodTypes.add(HealthFoodType.BLOOD_PRESSURE); + if (memory) healthFoodTypes.add(HealthFoodType.MEMORY); + if (antioxidant) healthFoodTypes.add(HealthFoodType.ANTIOXIDANT); + if (gut) healthFoodTypes.add(HealthFoodType.GUT); + if (liver) healthFoodTypes.add(HealthFoodType.LIVER); + if (eye) healthFoodTypes.add(HealthFoodType.EYE); + if (joint) healthFoodTypes.add(HealthFoodType.JOINT); + if (sleep) healthFoodTypes.add(HealthFoodType.SLEEP); + if (stressFatigue) healthFoodTypes.add(HealthFoodType.STRESS_FATIGUE); + if (menopause) healthFoodTypes.add(HealthFoodType.MENOPAUSE); + if (prostate) healthFoodTypes.add(HealthFoodType.PROSTATE); + if (urinary) healthFoodTypes.add(HealthFoodType.URINARY); + if (energy) healthFoodTypes.add(HealthFoodType.ENERGY); + if (bone) healthFoodTypes.add(HealthFoodType.BONE); + if (muscle) healthFoodTypes.add(HealthFoodType.MUSCLE); + if (cognition) healthFoodTypes.add(HealthFoodType.COGNITION); + if (stomach) healthFoodTypes.add(HealthFoodType.STOMACH); + if (oral) healthFoodTypes.add(HealthFoodType.ORAL); + if (hair) healthFoodTypes.add(HealthFoodType.HAIR); + if (growth) healthFoodTypes.add(HealthFoodType.GROWTH); + if (bloodPressure) healthFoodTypes.add(HealthFoodType.BLOOD_PRESSURE); + if (urination) healthFoodTypes.add(HealthFoodType.URINATION); + if (folate) healthFoodTypes.add(HealthFoodType.FOLATE); + if (nose) healthFoodTypes.add(HealthFoodType.NOSE); + if (maleHealth) healthFoodTypes.add(HealthFoodType.MALE_HEALTH); + if (electrolyte) healthFoodTypes.add(HealthFoodType.ELECTROLYTE); + if (dietaryFiber) healthFoodTypes.add(HealthFoodType.DIETARY_FIBER); + if (essentialFattyAcid) healthFoodTypes.add(HealthFoodType.ESSENTIAL_FATTY_ACID); + + return healthFoodTypes; + } +} From 8efd928bfa368f637032eb9e08f3f66c4a4233b6 Mon Sep 17 00:00:00 2001 From: zyovn Date: Mon, 12 May 2025 02:18:03 +0900 Subject: [PATCH 137/309] =?UTF-8?q?#33=20feat:=20=EA=B1=B4=EA=B0=95?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=8B=9D=ED=92=88=20open=20ai=20prompt=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/OpenAiClient.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 8b705e2..8cc378d 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -5,6 +5,7 @@ import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; @@ -147,6 +148,28 @@ public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { return callWithStructuredOutput(request, prompt, CosmeticResponse.class); } + public HealthFoodAiResponse explainHealthFood(ProductAnalysisRequest request) { + String system = """ + You are a health food ingredient analysis expert. + You specialize in identifying functional health food ingredients based on product labels or ingredient lists. + Do not include explanations, summaries, or any other text. + """; + + String user = """ + Analyze the provided product's ingredient label image. + + Check if any of the following ingredients are present: + + %s + + If any are found, list only the matched ingredient names separated by commas. + If none are found, reply with "None". + """; + + ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); + return callWithStructuredOutput(request, prompt, HealthFoodAiResponse.class); + } + private T callWithStructuredOutput(ProductAnalysisRequest request, ImageAnalysisPrompt prompt, Class clazz) { BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); From 5d584f51817d21df59d305ec8147a48272b4d1fa Mon Sep 17 00:00:00 2001 From: zyovn Date: Mon, 12 May 2025 21:09:49 +0900 Subject: [PATCH 138/309] =?UTF-8?q?#33=20feat:=20=EA=B1=B4=EA=B0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=8B=9D=ED=92=88=20=ED=9A=A8=EB=8A=A5=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EB=B6=84=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HealthFoodService.java | 37 ++++++++++ .../healthfood/dto/HealthFoodAiResponse.java | 72 +------------------ .../mapper/HealthFoodMapper.java | 8 +++ .../persistence/HealthFoodRepository.java | 8 +++ .../presentation/HealthFoodController.java | 16 +++-- .../swagger/HealthFoodSwagger.java | 18 +++++ .../infrastructure/OpenAiClient.java | 37 +++++++--- 7 files changed, 113 insertions(+), 83 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 91ca6a1..9956a9c 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -1,17 +1,26 @@ package com.webeye.backend.healthfood.application; import com.webeye.backend.healthfood.domain.HealthFood; +import com.webeye.backend.healthfood.domain.HealthFoodKeyword; +import com.webeye.backend.healthfood.domain.Keyword; +import com.webeye.backend.healthfood.domain.type.HealthFoodType; +import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; import com.webeye.backend.healthfood.dto.HealthFoodResponse; import com.webeye.backend.healthfood.infrastructure.client.HealthFoodClient; import com.webeye.backend.healthfood.infrastructure.mapper.HealthFoodMapper; import com.webeye.backend.healthfood.infrastructure.persistence.HealthFoodRepository; +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Arrays; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class HealthFoodService { @@ -22,6 +31,7 @@ public class HealthFoodService { @Value("${open-api.health-food.service-key}") private String serviceKey; + private final OpenAiClient openAiClient; private final HealthFoodClient healthFoodClient; private final HealthFoodRepository healthFoodRepository; @@ -39,4 +49,31 @@ public HealthFoodResponse.I2710 callHealthFoodApi() { return HealthFoodMapper.toResponseList(healthFoods, i2710.totalCount()); } + + @Transactional(readOnly = true) + public HealthFoodAiResponse analyzeHealthFood(ProductAnalysisRequest request) { + List ingredients = healthFoodRepository.findAllItemNames(); + + String extractedText = openAiClient.explainHealthFood(request, ingredients); + + List matchedIngredients = Arrays.stream(extractedText.split(",")) + .map(String::trim) + .filter(ingredients::contains) + .distinct() + .toList(); + + List types = mapHealthFoodTypes(matchedIngredients); + + return HealthFoodMapper.toResponse(types); + } + + private List mapHealthFoodTypes(List ingredients) { + return ingredients.stream() + .flatMap(ingredient -> healthFoodRepository.findByItemNameContaining(ingredient).stream()) + .flatMap(healthFood -> healthFood.getHealthFoodKeywords().stream()) + .map(HealthFoodKeyword::getKeyword) + .map(Keyword::getType) + .distinct() + .toList(); + } } diff --git a/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java index 12f229b..74996d0 100644 --- a/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java +++ b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java @@ -4,81 +4,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; -import java.util.ArrayList; import java.util.List; @Builder @Schema(description = "건강 기능 식품 분석") public record HealthFoodAiResponse( - boolean immune, - boolean skin, - boolean blood, - boolean bodyFat, - boolean bloodSugar, - boolean memory, - boolean antioxidant, - boolean gut, - boolean liver, - boolean eye, - boolean joint, - boolean sleep, - boolean stressFatigue, - boolean menopause, - boolean prostate, - boolean urinary, - boolean energy, - boolean bone, - boolean muscle, - boolean cognition, - boolean stomach, - boolean oral, - boolean hair, - boolean growth, - boolean bloodPressure, - boolean urination, - boolean folate, - boolean nose, - boolean maleHealth, - boolean electrolyte, - boolean dietaryFiber, - boolean essentialFattyAcid + List healthFoodTypes ) { - public List getHealthFoodTypes() { - List healthFoodTypes = new ArrayList<>(); - - if (immune) healthFoodTypes.add(HealthFoodType.IMMUNE); - if (skin) healthFoodTypes.add(HealthFoodType.SKIN); - if (blood) healthFoodTypes.add(HealthFoodType.BLOOD); - if (bodyFat) healthFoodTypes.add(HealthFoodType.BODY_FAT); - if (bloodPressure) healthFoodTypes.add(HealthFoodType.BLOOD_PRESSURE); - if (memory) healthFoodTypes.add(HealthFoodType.MEMORY); - if (antioxidant) healthFoodTypes.add(HealthFoodType.ANTIOXIDANT); - if (gut) healthFoodTypes.add(HealthFoodType.GUT); - if (liver) healthFoodTypes.add(HealthFoodType.LIVER); - if (eye) healthFoodTypes.add(HealthFoodType.EYE); - if (joint) healthFoodTypes.add(HealthFoodType.JOINT); - if (sleep) healthFoodTypes.add(HealthFoodType.SLEEP); - if (stressFatigue) healthFoodTypes.add(HealthFoodType.STRESS_FATIGUE); - if (menopause) healthFoodTypes.add(HealthFoodType.MENOPAUSE); - if (prostate) healthFoodTypes.add(HealthFoodType.PROSTATE); - if (urinary) healthFoodTypes.add(HealthFoodType.URINARY); - if (energy) healthFoodTypes.add(HealthFoodType.ENERGY); - if (bone) healthFoodTypes.add(HealthFoodType.BONE); - if (muscle) healthFoodTypes.add(HealthFoodType.MUSCLE); - if (cognition) healthFoodTypes.add(HealthFoodType.COGNITION); - if (stomach) healthFoodTypes.add(HealthFoodType.STOMACH); - if (oral) healthFoodTypes.add(HealthFoodType.ORAL); - if (hair) healthFoodTypes.add(HealthFoodType.HAIR); - if (growth) healthFoodTypes.add(HealthFoodType.GROWTH); - if (bloodPressure) healthFoodTypes.add(HealthFoodType.BLOOD_PRESSURE); - if (urination) healthFoodTypes.add(HealthFoodType.URINATION); - if (folate) healthFoodTypes.add(HealthFoodType.FOLATE); - if (nose) healthFoodTypes.add(HealthFoodType.NOSE); - if (maleHealth) healthFoodTypes.add(HealthFoodType.MALE_HEALTH); - if (electrolyte) healthFoodTypes.add(HealthFoodType.ELECTROLYTE); - if (dietaryFiber) healthFoodTypes.add(HealthFoodType.DIETARY_FIBER); - if (essentialFattyAcid) healthFoodTypes.add(HealthFoodType.ESSENTIAL_FATTY_ACID); - - return healthFoodTypes; - } } diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java index 63470b0..2ea070c 100644 --- a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java @@ -1,6 +1,8 @@ package com.webeye.backend.healthfood.infrastructure.mapper; import com.webeye.backend.healthfood.domain.HealthFood; +import com.webeye.backend.healthfood.domain.type.HealthFoodType; +import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; import com.webeye.backend.healthfood.dto.HealthFoodResponse; import java.util.List; @@ -35,4 +37,10 @@ public static HealthFoodResponse.I2710 toResponseList(List healthFoo return new HealthFoodResponse.I2710(totalCount, i2710List); } + + public static HealthFoodAiResponse toResponse(List types) { + return HealthFoodAiResponse.builder() + .healthFoodTypes(types) + .build(); + } } diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java index 8ecf8da..a9d9402 100644 --- a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java @@ -2,6 +2,14 @@ import com.webeye.backend.healthfood.domain.HealthFood; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; public interface HealthFoodRepository extends JpaRepository { + @Query("SELECT h.itemName FROM HealthFood h") + List findAllItemNames(); + + Optional findByItemNameContaining(String itemName); } diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java index 218b0b6..9d1ddc6 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java @@ -2,14 +2,15 @@ import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.healthfood.application.HealthFoodService; +import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; import com.webeye.backend.healthfood.dto.HealthFoodResponse; import com.webeye.backend.healthfood.presentation.swagger.HealthFoodSwagger; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import static com.webeye.backend.global.dto.response.type.SuccessCode.HEALTH_FOOD_API_SUCCESS; @@ -26,4 +27,11 @@ public class HealthFoodController implements HealthFoodSwagger { public SuccessResponse callHealthFoodApi() { return SuccessResponse.of(HEALTH_FOOD_API_SUCCESS, healthFoodService.callHealthFoodApi()); } + + @Override + @ResponseStatus(HttpStatus.OK) + @PostMapping("/keywords") + public SuccessResponse analyzeHealthFood(@Valid @RequestBody ProductAnalysisRequest request) { + return SuccessResponse.of(HEALTH_FOOD_API_SUCCESS, healthFoodService.analyzeHealthFood(request)); + } } diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java b/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java index 081878f..bdde039 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java @@ -1,11 +1,15 @@ package com.webeye.backend.healthfood.presentation.swagger; import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; import com.webeye.backend.healthfood.dto.HealthFoodResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.RequestBody; @Tag(name = "[건강 기능 식품]", description = "건강 기능 식품 관련 API") public interface HealthFoodSwagger { @@ -20,4 +24,18 @@ public interface HealthFoodSwagger { ) }) SuccessResponse callHealthFoodApi(); + + @Operation( + summary = "건강 기능 식품 효능 키워드 분석", + description = "해당 건강 기능 식품의 효능을 분석하고 키워드를 표출합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "건강 기능 식품 효능 키워드 분석이 성공적으로 실행되었습니다." + ) + }) + SuccessResponse analyzeHealthFood( + @Valid @RequestBody ProductAnalysisRequest request + ); } diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 8cc378d..14153ad 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -18,6 +18,7 @@ import org.springframework.util.MimeType; import java.net.MalformedURLException; +import java.util.List; import static com.webeye.backend.global.error.ErrorCode.FILE_EXTENSION_NOT_FOUND; import static com.webeye.backend.global.error.ErrorCode.INVALID_IMAGE_URL; @@ -148,26 +149,25 @@ public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { return callWithStructuredOutput(request, prompt, CosmeticResponse.class); } - public HealthFoodAiResponse explainHealthFood(ProductAnalysisRequest request) { + public String explainHealthFood(ProductAnalysisRequest request, List ingredients) { String system = """ You are a health food ingredient analysis expert. You specialize in identifying functional health food ingredients based on product labels or ingredient lists. Do not include explanations, summaries, or any other text. """; - String user = """ - Analyze the provided product's ingredient label image. - - Check if any of the following ingredients are present: + String user = String.format(""" + If the attached images contain 'nutrition information', please provide the amount of each ingredient in the format I sent. + Check if any of the following ingredients are present: %s If any are found, list only the matched ingredient names separated by commas. If none are found, reply with "None". - """; - + """, String.join(", ", ingredients) + ); ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request, prompt, HealthFoodAiResponse.class); + return callWithRawOutput(request, prompt); } private T callWithStructuredOutput(ProductAnalysisRequest request, ImageAnalysisPrompt prompt, Class clazz) { @@ -193,6 +193,27 @@ private T callWithStructuredOutput(ProductAnalysisRequest request, ImageAnal return outputConverter.convert(response); } + private String callWithRawOutput(ProductAnalysisRequest request, ImageAnalysisPrompt prompt) { + String response = chatClient.prompt() + .user(promptUserSpec -> { + try { + promptUserSpec.text(prompt.user()); + for (String imageUrl : request.urls()) { + MimeType extension = ImageMimeType.fromExtension(extractFileExtension(imageUrl)); + promptUserSpec.media(extension, new UrlResource(imageUrl)); + } + } catch (MalformedURLException exception) { + log.error("MalformedURLException: callWithStructuredOutput() 에서 발생"); + throw new BusinessException(INVALID_IMAGE_URL); + } + }) + .system(prompt.system()) + .call() + .content(); + + return response; + } + private String extractFileExtension(String url) { String fileName = url.substring(url.lastIndexOf('/') + 1); int dotIndex = fileName.lastIndexOf('.'); From 1910b52986a49ab31cdc910278f9824d4aeab70f Mon Sep 17 00:00:00 2001 From: zyovn Date: Mon, 12 May 2025 21:13:33 +0900 Subject: [PATCH 139/309] =?UTF-8?q?#33=20refactor:=20successCode=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/global/dto/response/type/SuccessCode.java | 1 + .../backend/healthfood/presentation/HealthFoodController.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index 1afa92b..c6f3cb7 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -34,6 +34,7 @@ public enum SuccessCode { // health food HEALTH_FOOD_API_SUCCESS(200, "Health Food API success"), + HEALTH_FOOD_ANALYSIS_SUCCESS(200, "Health Food analysis success"), ; diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java index 9d1ddc6..77f07e2 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.*; import static com.webeye.backend.global.dto.response.type.SuccessCode.HEALTH_FOOD_API_SUCCESS; +import static com.webeye.backend.global.dto.response.type.SuccessCode.HEALTH_FOOD_ANALYSIS_SUCCESS; @RestController @RequiredArgsConstructor @@ -32,6 +33,6 @@ public SuccessResponse callHealthFoodApi() { @ResponseStatus(HttpStatus.OK) @PostMapping("/keywords") public SuccessResponse analyzeHealthFood(@Valid @RequestBody ProductAnalysisRequest request) { - return SuccessResponse.of(HEALTH_FOOD_API_SUCCESS, healthFoodService.analyzeHealthFood(request)); + return SuccessResponse.of(HEALTH_FOOD_ANALYSIS_SUCCESS, healthFoodService.analyzeHealthFood(request)); } } From 2e10dc11de981b1c78af2f9a67f81d1b7dbd833f Mon Sep 17 00:00:00 2001 From: zyovn Date: Mon, 12 May 2025 22:41:07 +0900 Subject: [PATCH 140/309] =?UTF-8?q?#33=20feat:=20product-productHealthFood?= =?UTF-8?q?-HealthFood=20=EB=8B=A4=EB=8C=80=EB=8B=A4=20=EA=B4=80=EA=B3=84?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/healthfood/domain/HealthFood.java | 9 ++++ .../backend/product/domain/Product.java | 9 ++++ .../product/domain/ProductHealthfood.java | 44 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java index f06d636..c83bac9 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java @@ -1,6 +1,7 @@ package com.webeye.backend.healthfood.domain; import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.product.domain.ProductHealthfood; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -29,6 +30,9 @@ public class HealthFood extends BaseEntity { @OneToMany(mappedBy = "healthFood", cascade = CascadeType.ALL) private List healthFoodKeywords= new ArrayList<>(); + @OneToMany(mappedBy = "healthFood", cascade = CascadeType.ALL) + private List healthfoods = new ArrayList<>(); + @Builder public HealthFood(String itemName, String functionality) { this.itemName = itemName; @@ -39,4 +43,9 @@ public void addKeyword(HealthFoodKeyword healthFoodKeyword) { healthFoodKeywords.add(healthFoodKeyword); healthFoodKeyword.associateWithHealthFood(this); } + + public void addProduct(ProductHealthfood healthFood) { + healthfoods.add(healthFood); + healthFood.associateWithHealthFood(this); + } } diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index a473901..7c1a701 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -1,6 +1,7 @@ package com.webeye.backend.product.domain; import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.healthfood.domain.HealthFood; import jakarta.persistence.*; import lombok.*; @@ -21,6 +22,9 @@ public class Product extends BaseEntity { @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) private List nutrients = new ArrayList<>(); + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + private List healthFoods = new ArrayList<>(); + @Builder public Product(String id) { this.id = id; @@ -35,4 +39,9 @@ public void addAllergy(ProductAllergy allergy) { this.allergies.add(allergy); allergy.associateWithProduct(this); } + + public void addHealthFood(ProductHealthfood healthFood) { + this.healthFoods.add(healthFood); + healthFood.associateWithProduct(this); + } } \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java b/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java new file mode 100644 index 0000000..2fc868a --- /dev/null +++ b/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java @@ -0,0 +1,44 @@ +package com.webeye.backend.product.domain; + +import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.healthfood.domain.HealthFood; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductHealthfood extends BaseEntity { + + @Id + @Column(name = "product_health_food_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "health_food_id") + private HealthFood healthFood; + + @Builder + public ProductHealthfood(Product product, HealthFood healthFood) { + this.product = product; + this.healthFood = healthFood; + } + + public void associateWithProduct(Product product) { + this.product = product; + product.getHealthFoods().add(this); + } + + public void associateWithHealthFood(HealthFood healthFood) { + this.healthFood = healthFood; + healthFood.getHealthfoods().add(this); + } +} From 9a10b1da1022960e7a20f734299db5ac493a639a Mon Sep 17 00:00:00 2001 From: zyovn Date: Mon, 12 May 2025 22:43:59 +0900 Subject: [PATCH 141/309] #33 feat: productHealthFoodRepository --- .../product/persistent/ProductHealthFoodRepository.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/com/webeye/backend/product/persistent/ProductHealthFoodRepository.java diff --git a/src/main/java/com/webeye/backend/product/persistent/ProductHealthFoodRepository.java b/src/main/java/com/webeye/backend/product/persistent/ProductHealthFoodRepository.java new file mode 100644 index 0000000..6c22ca7 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/persistent/ProductHealthFoodRepository.java @@ -0,0 +1,7 @@ +package com.webeye.backend.product.persistent; + +import com.webeye.backend.product.domain.ProductHealthfood; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductHealthFoodRepository extends JpaRepository { +} From 00cd4d7cf78eecea888aaf24a75c0f1fffcb8c3f Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 12 May 2025 22:54:57 +0900 Subject: [PATCH 142/309] =?UTF-8?q?#36=20remove:=20=EA=B8=B0=EC=A1=B4=20Op?= =?UTF-8?q?en=20API=20=ED=98=B8=EC=B6=9C=ED=95=B4=EC=84=9C=20=EC=9B=90?= =?UTF-8?q?=EC=9E=AC=EB=A3=8C=20=EC=98=81=EC=96=91=EC=84=B1=EB=B6=84=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A4=EB=8A=94=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RawMaterialService.java | 7 -- .../service/RawMaterialServiceImpl.java | 61 ------------ .../rawmaterial/dto/RawMaterialResponse.java | 85 ----------------- .../client/RawMaterialClient.java | 26 ----- .../mapper/RawMaterialMapper.java | 94 ------------------- .../persistence/RawMaterialRepository.java | 7 -- .../presentation/RawMaterialController.java | 32 ------- .../swagger/RawMaterialSwagger.java | 27 ------ 8 files changed, 339 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/infrastructure/persistence/RawMaterialRepository.java delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java delete mode 100644 src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java deleted file mode 100644 index 1ded6a0..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.webeye.backend.rawmaterial.application.service; - -import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; - -public interface RawMaterialService { - RawMaterialResponse.Body callRawMaterialAPI(Integer pageNo, Integer numOfRows); -} diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java b/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java deleted file mode 100644 index 45d2eec..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/application/service/RawMaterialServiceImpl.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.webeye.backend.rawmaterial.application.service; - -import com.webeye.backend.rawmaterial.infrastructure.mapper.RawMaterialMapper; -import com.webeye.backend.rawmaterial.domain.RawMaterial; -import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; -import com.webeye.backend.rawmaterial.infrastructure.client.RawMaterialClient; -import com.webeye.backend.rawmaterial.infrastructure.persistence.RawMaterialRepository; -import com.webeye.backend.global.error.BusinessException; -import com.webeye.backend.global.error.ErrorCode; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class RawMaterialServiceImpl implements RawMaterialService { - - @Value("${open-api.raw-material.service-key}") - private String serviceKey; - - private final RawMaterialClient rawMaterialClient; - private final RawMaterialRepository rawMaterialRepository; - - @Override - @Transactional - public RawMaterialResponse.Body callRawMaterialAPI(Integer pageNo, Integer numOfRows) { - RawMaterialResponse response = rawMaterialClient.getRawMaterialInfo(serviceKey, pageNo, numOfRows, "json"); - - RawMaterialResponse.Response innerResponse = response.response(); - - List items = innerResponse.body().items(); - - validateRawMaterial(innerResponse, items); - - Integer totalCount = Integer.parseInt(innerResponse.body().totalCount()); - - List rawMaterials = RawMaterialMapper.toEntityList(items); - - // TODO: 전처리 후 저장 - // List savedRawMaterials = rawMaterialRepository.saveAll(rawMaterials); - - return RawMaterialMapper.ofList(rawMaterials, pageNo, numOfRows, totalCount); - } - - private void validateRawMaterial(RawMaterialResponse.Response response, List items) { - // Open API 응답 실패 - if (response == null || response.body() == null) { - throw new BusinessException(ErrorCode.OPEN_API_RESPONSE_NULL); - } - - // items not found - if (items == null || response.body().items().isEmpty()) { - throw new BusinessException(ErrorCode.OPEN_API_DATA_MISSING); - } - } -} diff --git a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java b/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java deleted file mode 100644 index e0f44b6..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/dto/RawMaterialResponse.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.webeye.backend.rawmaterial.dto; - -import io.swagger.v3.oas.annotations.media.Schema; - -import java.util.List; - -@Schema(description = "원재료 영양 성분") -public record RawMaterialResponse( - Response response -) { - - public record Response( - Body body - ) {} - - public record Body( - @Schema(description = "원재료 목록") - List items, - @Schema(description = "페이지 번호", example = "1") - String pageNo, - @Schema(description = "페이지 내 데이터 개수", example = "10") - String numOfRows, - @Schema(description = "총 데이터 개수", example = "3500") - String totalCount - ) {} - - public record Item( - @Schema(description = "원재료 영양성분 ID", example = "1") - Long rawMaterialId, - @Schema(description = "식품 코드", example = "R211-009111601-0001") - String foodCd, - @Schema(description = "식품명", example = "청어_암컷,육_생것_포항_8월") - String foodNm, - @Schema(description = "영양성분함량기준량", example = "100g") - String nutConSrtrQua, - @Schema(description = "에너지", example = "90") - Double enerc, - @Schema(description = "수분", example = "40.7") - Double water, - @Schema(description = "단백질", example = "18.21") - Double prot, - @Schema(description = "지방", example = "3.14") - Double fatce, - @Schema(description = "회분", example = "1.2") - Double ash, - @Schema(description = "탄수화물", example = "0.61") - Double chocdf, - @Schema(description = "당류", example = "2.64") - Double sugar, - @Schema(description = "식이섬유", example = "21.7") - Double fibtg, - @Schema(description = "칼슘", example = "156") - Double ca, - @Schema(description = "철", example = "10.4") - Double fe, - @Schema(description = "인", example = "244") - Double p, - @Schema(description = "칼륨", example = "250") - Double k, - @Schema(description = "나트륨", example = "1300") - Double nat, - @Schema(description = "비타민 A", example = "1007") - Double vitaRae, - @Schema(description = "레티놀", example = "12") - Double retol, - @Schema(description = "베타카르틴", example = "2698") - Double cartb, - @Schema(description = "티아민", example = "0.968") - Double thia, - @Schema(description = "리보플라빈", example = "0.11") - Double ribf, - @Schema(description = "니아신", example = "6.3") - Double nia, - @Schema(description = "비타민 C", example = "1.75") - Double vitc, - @Schema(description = "비타민 D", example = "48") - Double vitd, - @Schema(description = "콜레스테롤", example = "22.57") - Double chole, - @Schema(description = "포화지방산", example = "0.83") - Double fasat, - @Schema(description = "트랜스지방산", example = "0") - Double fartn - ) {} -} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java deleted file mode 100644 index a9e1e97..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/client/RawMaterialClient.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.webeye.backend.rawmaterial.infrastructure.client; - -import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; -import com.webeye.backend.global.config.OpenFeignConfig; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; - -@FeignClient( - name = "rawMaterialClient", - url = "${open-api.raw-material.url}", - configuration = OpenFeignConfig.class -) -public interface RawMaterialClient { - - @GetMapping( - value = "${open-api.raw-material.value}", - produces = "application/json" - ) - RawMaterialResponse getRawMaterialInfo( - @RequestParam("serviceKey") String serviceKey, - @RequestParam("pageNo") int pageNo, - @RequestParam("numOfRows") int numOfRows, - @RequestParam("type") String type - ); -} diff --git a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java deleted file mode 100644 index 0b0b98c..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/mapper/RawMaterialMapper.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.webeye.backend.rawmaterial.infrastructure.mapper; - -import com.webeye.backend.rawmaterial.domain.RawMaterial; -import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; - -import java.util.List; -import java.util.stream.Collectors; - -public class RawMaterialMapper { - - public static RawMaterial toEntity(RawMaterialResponse.Item item) { - return RawMaterial.builder() - .foodCd(item.foodCd()) - .foodNm(item.foodNm()) - .nutConSrtrQua(item.nutConSrtrQua()) - .enerc(item.enerc()) - .water(item.water()) - .prot(item.prot()) - .fatce(item.fatce()) - .ash(item.ash()) - .chocdf(item.chocdf()) - .sugar(item.sugar()) - .fibtg(item.fibtg()) - .ca(item.ca()) - .fe(item.fe()) - .p(item.p()) - .k(item.k()) - .nat(item.nat()) - .vitaRae(item.vitaRae()) - .retol(item.retol()) - .cartb(item.cartb()) - .thia(item.thia()) - .ribf(item.ribf()) - .nia(item.nia()) - .vitc(item.vitc()) - .vitd(item.vitd()) - .chole(item.chole()) - .fasat(item.fasat()) - .fartn(item.fartn()) - .build(); - } - - public static RawMaterialResponse.Item of(RawMaterial rawMaterial) { - return new RawMaterialResponse.Item( - rawMaterial.getId(), - rawMaterial.getFoodCd(), - rawMaterial.getFoodNm(), - rawMaterial.getNutConSrtrQua(), - rawMaterial.getEnerc(), - rawMaterial.getWater(), - rawMaterial.getProt(), - rawMaterial.getFatce(), - rawMaterial.getAsh(), - rawMaterial.getChocdf(), - rawMaterial.getSugar(), - rawMaterial.getFibtg(), - rawMaterial.getCa(), - rawMaterial.getFe(), - rawMaterial.getP(), - rawMaterial.getK(), - rawMaterial.getNat(), - rawMaterial.getVitaRae(), - rawMaterial.getRetol(), - rawMaterial.getCartb(), - rawMaterial.getThia(), - rawMaterial.getRibf(), - rawMaterial.getNia(), - rawMaterial.getVitc(), - rawMaterial.getVitd(), - rawMaterial.getChole(), - rawMaterial.getFasat(), - rawMaterial.getFartn() - ); - } - - public static List toEntityList(List items) { - return items.stream() - .map(RawMaterialMapper::toEntity) - .collect(Collectors.toList()); - } - - public static RawMaterialResponse.Body ofList(List list, Integer pageNo, Integer numOfRows, Integer totalCount) { - List itemList = list.stream() - .map(RawMaterialMapper::of) - .collect(Collectors.toList()); - - return new RawMaterialResponse.Body( - itemList, - String.valueOf(pageNo), - String.valueOf(numOfRows), - String.valueOf(totalCount) - ); - } -} diff --git a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/persistence/RawMaterialRepository.java b/src/main/java/com/webeye/backend/rawmaterial/infrastructure/persistence/RawMaterialRepository.java deleted file mode 100644 index 93f7eb4..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/infrastructure/persistence/RawMaterialRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.webeye.backend.rawmaterial.infrastructure.persistence; - -import com.webeye.backend.rawmaterial.domain.RawMaterial; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface RawMaterialRepository extends JpaRepository { -} diff --git a/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java b/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java deleted file mode 100644 index 95e9978..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/presentation/RawMaterialController.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.webeye.backend.rawmaterial.presentation; - -import com.webeye.backend.rawmaterial.application.service.RawMaterialService; -import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; -import com.webeye.backend.rawmaterial.presentation.swagger.RawMaterialSwagger; -import com.webeye.backend.global.dto.response.SuccessResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -import static com.webeye.backend.global.dto.response.type.SuccessCode.RAW_MATERIAL_API_SUCCESS; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/v1/raw-materials") -public class RawMaterialController implements RawMaterialSwagger { - - private final RawMaterialService rawMaterialService; - - @Override - @ResponseStatus(HttpStatus.OK) - @GetMapping - public SuccessResponse callRawMaterialApi( - @RequestParam Integer pageNo, - @RequestParam Integer numOfRows - ) { - return SuccessResponse.of( - RAW_MATERIAL_API_SUCCESS, - rawMaterialService.callRawMaterialAPI(pageNo, numOfRows) - ); - } -} diff --git a/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java b/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java deleted file mode 100644 index 9c27300..0000000 --- a/src/main/java/com/webeye/backend/rawmaterial/presentation/swagger/RawMaterialSwagger.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.webeye.backend.rawmaterial.presentation.swagger; - -import com.webeye.backend.rawmaterial.dto.RawMaterialResponse; -import com.webeye.backend.global.dto.response.SuccessResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.RequestParam; - -@Tag(name = "[원재료 주의 영양 성분]", description = "원재료 주의 영양 성분 표시 관련 API") -public interface RawMaterialSwagger { - @Operation( - summary = "원재료 영양 성분 OPEN API 호출", - description = "공공 데이터 포털 원재료 영양 성분 OPEN API를 호출합니다." - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "OPEN API가 성공적으로 호출되었습니다." - ) - }) - SuccessResponse callRawMaterialApi( - @RequestParam Integer pageNo, - @RequestParam Integer numOfRows - ); -} From 0c36e9a954a1b9955dc690532afaba95ec1f3419 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 12 May 2025 22:55:51 +0900 Subject: [PATCH 143/309] =?UTF-8?q?#36=20feat:=20RawMaterial=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=BB=AC=EB=9F=BC=EB=A7=8C=20=EB=91=90=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rawmaterial/domain/RawMaterial.java | 138 +++++------------- 1 file changed, 38 insertions(+), 100 deletions(-) diff --git a/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java b/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java index 60a32bf..13c525b 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java +++ b/src/main/java/com/webeye/backend/rawmaterial/domain/RawMaterial.java @@ -17,118 +17,56 @@ public class RawMaterial extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "food_cd") - private String foodCd; // 식품 코드 + private String name; - @Column(name = "food_nm") - private String foodNm; // 식품명 + private Double protein; // 단백질 - @Column(name = "nut_con_srtr_qua") - private String nutConSrtrQua; // 영양성분함량기준량 + private Double fat; // 지방 - @Column(name = "enerc") - private Double enerc; // 에너지 + private Double carbohydrate; // 탄수화물 - @Column(name = "water") - private Double water; // 수분 + private Double sugars; // 당류 - @Column(name = "prot") - private Double prot; // 단백질 + private Double calcium; // 칼슘 - @Column(name = "fatce") - private Double fatce; // 지방 + private Double phosphorus; // 인 - @Column(name = "ash") - private Double ash; // 회분 + private Double sodium; // 나트륨 - @Column(name = "chocdf") - private Double chocdf; // 탄수화물 + private Double niacin; // 나이아신 - @Column(name = "sugar") - private Double sugar; // 당류 + private Double cholesterol; // 콜레스테롤 - @Column(name = "fibtg") - private Double fibtg; // 식이섬유 + private Double saturatedFat; // 포화지방 - @Column(name = "ca") - private Double ca; // 칼슘 - - @Column(name = "fe") - private Double fe; // 철 - - @Column(name = "p") - private Double p; // 인 - - @Column(name = "k") - private Double k; // 칼륨 - - @Column(name = "nat") - private Double nat; // 나트륨 - - @Column(name = "vita_rae") - private Double vitaRae; // 비타민 A - - @Column(name = "retol") - private Double retol; // 레티놀 - - @Column(name = "cartb") - private Double cartb; // 베타카르틴 - - @Column(name = "thia") - private Double thia; // 티아민 - - @Column(name = "ribf") - private Double ribf; // 리보플라빈 - - @Column(name = "nia") - private Double nia; // 니아신 - - @Column(name = "vitc") - private Double vitc; // 비타민 C - - @Column(name = "vitd") - private Double vitd; // 비타민 D - - @Column(name = "chole") - private Double chole; // 콜레스테롤 - - @Column(name = "fasat") - private Double fasat; // 포화지방산 - - @Column(name = "fartn") - private Double fartn; // 트랜스지방산 + private Double transFat; // 트랜스지방 @Builder - private RawMaterial(Long id, String foodCd, String foodNm, String nutConSrtrQua, Double enerc, Double water, Double prot, Double fatce, Double ash, - Double chocdf, Double sugar, Double fibtg, Double ca, Double fe, Double p, Double k, Double nat, Double vitaRae, Double retol, - Double cartb, Double thia, Double ribf, Double nia, Double vitc, Double vitd, Double chole, Double fasat, Double fartn) { - this.id = id; - this.foodCd = foodCd; - this.foodNm = foodNm; - this.nutConSrtrQua = nutConSrtrQua; - this.enerc = enerc; - this.water = water; - this.prot = prot; - this.fatce = fatce; - this.ash = ash; - this.chocdf = chocdf; - this.sugar = sugar; - this.fibtg = fibtg; - this.ca = ca; - this.fe = fe; - this.p = p; - this.k = k; - this.nat = nat; - this.vitaRae = vitaRae; - this.retol = retol; - this.cartb = cartb; - this.thia = thia; - this.ribf = ribf; - this.nia = nia; - this.vitc = vitc; - this.vitd = vitd; - this.chole = chole; - this.fasat = fasat; - this.fartn = fartn; + private RawMaterial( + String name, + Double protein, + Double fat, + Double carbohydrate, + Double sugars, + Double calcium, + Double phosphorus, + Double sodium, + Double niacin, + Double cholesterol, + Double saturatedFat, + Double transFat + ) { + this.name = name; + this.protein = protein; + this.fat = fat; + this.carbohydrate = carbohydrate; + this.sugars = sugars; + this.calcium = calcium; + this.phosphorus = phosphorus; + this.sodium = sodium; + this.niacin = niacin; + this.cholesterol = cholesterol; + this.saturatedFat = saturatedFat; + this.transFat = transFat; } } \ No newline at end of file From f50693e3763c95fdbc50f85f2bef4eb65b1a3036 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 12 May 2025 23:25:05 +0900 Subject: [PATCH 144/309] =?UTF-8?q?#37=20refactor:=20=EC=8B=9D=ED=92=88=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=82=B4=EC=9A=A9=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20DTO=EC=99=80=20=EA=B8=B0=ED=83=80=20?= =?UTF-8?q?=EC=A0=9C=ED=92=88=20=EB=B6=84=EC=84=9D=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?DTO=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 식품 분석 요청 DTO -> FoodProductAnalysisRequest 일반 제품 분석 요청 DTO -> ProductAnalysisRequest --- .../allergy/application/AllergyService.java | 6 ++--- .../presentation/AllergyController.java | 4 ++-- .../presentation/swagger/AllergySwagger.java | 4 ++-- .../cosmetic/application/CosmeticService.java | 2 +- .../application/CosmeticServiceImpl.java | 2 +- .../presentation/CosmeticController.java | 2 +- .../presentation/swagger/CosmeticSwagger.java | 2 +- .../application/ExplanationService.java | 2 +- .../dto/request/ProductAnalysisRequest.java | 19 +++++++++++++++ .../presentation/ExplanationController.java | 2 +- .../swagger/ExplanationSwagger.java | 2 +- .../infrastructure/OpenAiClient.java | 23 +++++++++++-------- .../application/NutritionService.java | 6 ++--- .../presentation/NutritionController.java | 4 ++-- .../swagger/NutritionSwagger.java | 4 ++-- .../product/application/ProductService.java | 6 ++--- ...t.java => FoodProductAnalysisRequest.java} | 4 ++-- .../product/dto/response/ProductResponse.java | 1 - .../presentation/ProductController.java | 4 ++-- .../presentation/swagger/ProductSwagger.java | 4 ++-- 20 files changed, 62 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/webeye/backend/explanation/dto/request/ProductAnalysisRequest.java rename src/main/java/com/webeye/backend/product/dto/request/{ProductAnalysisRequest.java => FoodProductAnalysisRequest.java} (90%) diff --git a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java index 90a6c81..021027b 100644 --- a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java +++ b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java @@ -1,7 +1,7 @@ package com.webeye.backend.allergy.application; import com.webeye.backend.allergy.dto.response.AllergyAiResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.product.domain.Product; import com.webeye.backend.product.domain.ProductAllergy; @@ -16,12 +16,12 @@ public class AllergyService { private final OpenAiClient openAiClient; private final ProductRepository productRepository; - public AllergyAiResponse analyzeAllergy(ProductAnalysisRequest request) { + public AllergyAiResponse analyzeAllergy(FoodProductAnalysisRequest request) { return openAiClient.explainAllergy(request); } @Transactional - public void saveProductAllergy(Product product, ProductAnalysisRequest request) { + public void saveProductAllergy(Product product, FoodProductAnalysisRequest request) { AllergyAiResponse response = analyzeAllergy(request); response.getAllergyTypes().forEach(allergyType -> diff --git a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java index c7e6726..439b3ec 100644 --- a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java +++ b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java @@ -4,7 +4,7 @@ import com.webeye.backend.allergy.dto.response.AllergyAiResponse; import com.webeye.backend.allergy.presentation.swagger.AllergySwagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -21,7 +21,7 @@ public class AllergyController implements AllergySwagger { @Override @ResponseStatus(HttpStatus.OK) @PostMapping(value = "") - public SuccessResponse allergyAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { + public SuccessResponse allergyAnalysis(@Valid @RequestBody FoodProductAnalysisRequest request) { return SuccessResponse.of(ALLERGY_ANALYSIS_SUCCESS ,allergyService.analyzeAllergy(request)); } } diff --git a/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java b/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java index dfc28e4..5b3ed49 100644 --- a/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java +++ b/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java @@ -2,7 +2,7 @@ import com.webeye.backend.allergy.dto.response.AllergyAiResponse; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -22,7 +22,7 @@ public interface AllergySwagger { ) }) SuccessResponse allergyAnalysis( - @RequestBody ProductAnalysisRequest request + @RequestBody FoodProductAnalysisRequest request ); } diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java index 65f452b..456359e 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java @@ -1,7 +1,7 @@ package com.webeye.backend.cosmetic.application; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; public interface CosmeticService { CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request); diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java index f2af544..ff34cfe 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java @@ -1,7 +1,7 @@ package com.webeye.backend.cosmetic.application; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java b/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java index 4772e29..c7785a2 100644 --- a/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java +++ b/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java @@ -3,8 +3,8 @@ import com.webeye.backend.cosmetic.application.CosmeticService; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.cosmetic.presentation.swagger.CosmeticSwagger; +import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.parameters.RequestBody; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java b/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java index d6f3dcf..ea8881e 100644 --- a/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java +++ b/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java @@ -1,8 +1,8 @@ package com.webeye.backend.cosmetic.presentation.swagger; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; diff --git a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java index 398ab7b..f5b1579 100644 --- a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java +++ b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java @@ -1,8 +1,8 @@ package com.webeye.backend.explanation.application; +import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/webeye/backend/explanation/dto/request/ProductAnalysisRequest.java b/src/main/java/com/webeye/backend/explanation/dto/request/ProductAnalysisRequest.java new file mode 100644 index 0000000..8475bc3 --- /dev/null +++ b/src/main/java/com/webeye/backend/explanation/dto/request/ProductAnalysisRequest.java @@ -0,0 +1,19 @@ +package com.webeye.backend.explanation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; + +import java.util.List; + +@Schema(description = "제품 분석") +@Builder +public record ProductAnalysisRequest( + @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") + String productId, + + @Schema(description = "상품 이미지 URL") + @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") + List urls +) { +} diff --git a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java index e75dea2..281d64e 100644 --- a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java +++ b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java @@ -1,11 +1,11 @@ package com.webeye.backend.explanation.presentation; import com.webeye.backend.explanation.application.ExplanationService; +import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.explanation.presentation.swagger.ExplanationSwagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java b/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java index aa212c0..29c6b93 100644 --- a/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java +++ b/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java @@ -1,9 +1,9 @@ package com.webeye.backend.explanation.presentation.swagger; +import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 8b705e2..53a8399 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -2,11 +2,12 @@ import com.webeye.backend.allergy.dto.response.AllergyAiResponse; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,6 +18,7 @@ import org.springframework.util.MimeType; import java.net.MalformedURLException; +import java.util.List; import static com.webeye.backend.global.error.ErrorCode.FILE_EXTENSION_NOT_FOUND; import static com.webeye.backend.global.error.ErrorCode.INVALID_IMAGE_URL; @@ -48,7 +50,7 @@ public PointExplanationResponse explainProductPoint(ProductAnalysisRequest reque """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request, prompt, PointExplanationResponse.class); + return callWithStructuredOutput(request.urls(), prompt, PointExplanationResponse.class); } @@ -60,16 +62,17 @@ public DetailExplanationResponse explainProductDetail(ProductAnalysisRequest req """; String user = """ + Key descriptive element: %s I have provided a product description image along with the key descriptive elements extracted from the image. Please generate a detailed explanation of the provided key descriptive element. """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request, prompt, DetailExplanationResponse.class); + return callWithStructuredOutput(request.urls(), prompt, DetailExplanationResponse.class); } - public AllergyAiResponse explainAllergy(ProductAnalysisRequest request) { + public AllergyAiResponse explainAllergy(FoodProductAnalysisRequest request) { String system = """ You are an OCR assistant that extracts and detects allergenic ingredients from Korean product label images. Always treat partial matches inside compound words as valid if they contain the full Korean name of an allergen. @@ -95,10 +98,10 @@ public AllergyAiResponse explainAllergy(ProductAnalysisRequest request) { """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request, prompt, AllergyAiResponse.class); + return callWithStructuredOutput(request.urls(), prompt, AllergyAiResponse.class); } - public NutritionAiResponse explainNutrition(ProductAnalysisRequest request) { + public NutritionAiResponse explainNutrition(FoodProductAnalysisRequest request) { String system = """ You are a nutrition description assistant. """; @@ -108,7 +111,7 @@ public NutritionAiResponse explainNutrition(ProductAnalysisRequest request) { """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request, prompt, NutritionAiResponse.class); + return callWithStructuredOutput(request.urls(), prompt, NutritionAiResponse.class); } public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { @@ -144,17 +147,17 @@ public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request, prompt, CosmeticResponse.class); + return callWithStructuredOutput(request.urls(), prompt, CosmeticResponse.class); } - private T callWithStructuredOutput(ProductAnalysisRequest request, ImageAnalysisPrompt prompt, Class clazz) { + private T callWithStructuredOutput(List urls, ImageAnalysisPrompt prompt, Class clazz) { BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); String response = chatClient.prompt() .user(promptUserSpec -> { try { promptUserSpec.text(prompt.user() + outputConverter.getFormat()); - for (String imageUrl : request.urls()) { + for (String imageUrl : urls) { MimeType extension = ImageMimeType.fromExtension(extractFileExtension(imageUrl)); promptUserSpec.media(extension, new UrlResource(imageUrl)); } diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index 09dd94f..f5d99b5 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -2,7 +2,7 @@ import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.nutrition.domain.Nutrient; import com.webeye.backend.nutrition.domain.type.NutrientType; @@ -27,7 +27,7 @@ public class NutritionService { private final NutrientRepository nutrientRepository; private final ProductRepository productRepository; - public NutritionAiResponse analyzeNutrition(ProductAnalysisRequest request) { + public NutritionAiResponse analyzeNutrition(FoodProductAnalysisRequest request) { return openAiClient.explainNutrition(request); } @@ -37,7 +37,7 @@ public Nutrient findByType(NutrientType type) { } @Transactional - public void saveProductNutrition(Product product, ProductAnalysisRequest request) { + public void saveProductNutrition(Product product, FoodProductAnalysisRequest request) { NutritionAiResponse response = analyzeNutrition(request); Map nutrientMap = extractNutrientMap(response); diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java index 0e12941..8287439 100644 --- a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java +++ b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java @@ -1,7 +1,7 @@ package com.webeye.backend.nutrition.presentation; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.application.NutritionService; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; import com.webeye.backend.nutrition.presentation.swagger.NutritionSwagger; @@ -21,7 +21,7 @@ public class NutritionController implements NutritionSwagger { @Override @ResponseStatus(HttpStatus.OK) @PostMapping(value = "") - public SuccessResponse nutritionAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { + public SuccessResponse nutritionAnalysis(@Valid @RequestBody FoodProductAnalysisRequest request) { return SuccessResponse.of(NUTRITION_ANALYSIS_SUCCESS, nutritionService.analyzeNutrition(request)); } } diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java b/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java index b6c160b..276e942 100644 --- a/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java +++ b/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java @@ -1,7 +1,7 @@ package com.webeye.backend.nutrition.presentation.swagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -22,6 +22,6 @@ public interface NutritionSwagger { ) }) SuccessResponse nutritionAnalysis( - @RequestBody ProductAnalysisRequest request + @RequestBody FoodProductAnalysisRequest request ); } diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 8827439..9e0794f 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -8,7 +8,7 @@ import com.webeye.backend.nutrition.dto.request.NutrientRecommendationRequest; import com.webeye.backend.nutrition.dto.response.NutrientRecommendationResponse; import com.webeye.backend.product.domain.ProductAllergy; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.application.NutritionService; import com.webeye.backend.product.domain.Product; import com.webeye.backend.product.dto.response.ProductResponse; @@ -30,7 +30,7 @@ public class ProductService { private final ProductRepository productRepository; @Transactional - public ProductResponse analyzeFoodProduct(ProductAnalysisRequest request) { + public ProductResponse analyzeFoodProduct(FoodProductAnalysisRequest request) { if (productRepository.existsById(request.productId())) { Product product = productRepository.findById(request.productId()) .orElseThrow(() -> new BusinessException(ErrorCode.PRODUCT_NOT_FOUND)); @@ -63,7 +63,7 @@ private List getAllergyResponse(Product product, List .toList(); } - private List getNutrientRecommendationResponse(ProductAnalysisRequest request, Product product) { + private List getNutrientRecommendationResponse(FoodProductAnalysisRequest request, Product product) { return nutrientRecommendationService.analyzeNutrientSufficiency(NutrientRecommendationRequest .builder().birthYear(request.birthYear()).gender(request.gender()).product(product).build()); } diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java similarity index 90% rename from src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java rename to src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java index 4554677..f7e6c51 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java @@ -8,9 +8,9 @@ import java.util.List; -@Schema(description = "이미지 분석 URL 요청") +@Schema(description = "사용자 정보 기반 식품 분석") @Builder -public record ProductAnalysisRequest( +public record FoodProductAnalysisRequest( @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") String productId, diff --git a/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java index 304bb88..ab55960 100644 --- a/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java +++ b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java @@ -1,7 +1,6 @@ package com.webeye.backend.product.dto.response; import com.webeye.backend.allergy.type.AllergyType; -import com.webeye.backend.nutrition.domain.type.NutrientType; import com.webeye.backend.nutrition.dto.response.NutrientRecommendationResponse; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/webeye/backend/product/presentation/ProductController.java b/src/main/java/com/webeye/backend/product/presentation/ProductController.java index f3f8862..044da19 100644 --- a/src/main/java/com/webeye/backend/product/presentation/ProductController.java +++ b/src/main/java/com/webeye/backend/product/presentation/ProductController.java @@ -1,7 +1,7 @@ package com.webeye.backend.product.presentation; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.product.application.ProductService; import com.webeye.backend.product.dto.response.ProductResponse; import com.webeye.backend.product.presentation.swagger.ProductSwagger; @@ -21,7 +21,7 @@ public class ProductController implements ProductSwagger { @Override @ResponseStatus(HttpStatus.OK) @PostMapping(value = "/foods") - public SuccessResponse foodAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { + public SuccessResponse foodAnalysis(@Valid @RequestBody FoodProductAnalysisRequest request) { return SuccessResponse.of(FOOD_PRODUCT_ANALYSIS_SUCCESS, productService.analyzeFoodProduct(request)); } diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java index baabbaf..3b04f36 100644 --- a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -1,7 +1,7 @@ package com.webeye.backend.product.presentation.swagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.product.dto.response.ProductResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -22,6 +22,6 @@ public interface ProductSwagger { ) }) SuccessResponse foodAnalysis( - @RequestBody ProductAnalysisRequest request + @RequestBody FoodProductAnalysisRequest request ); } From f45e5a989e8f1bd353b74a725d0cd08cd7e0f8ef Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 12 May 2025 23:29:56 +0900 Subject: [PATCH 145/309] =?UTF-8?q?#37=20refactor:=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=84=A4=EB=AA=85=20DTO=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ExplanationService.java | 3 ++- .../request/ProductDetailAnalysisRequest.java | 22 +++++++++++++++++++ .../presentation/ExplanationController.java | 3 ++- .../swagger/ExplanationSwagger.java | 3 ++- .../infrastructure/OpenAiClient.java | 7 +++--- 5 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/webeye/backend/explanation/dto/request/ProductDetailAnalysisRequest.java diff --git a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java index f5b1579..1bc6217 100644 --- a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java +++ b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java @@ -1,6 +1,7 @@ package com.webeye.backend.explanation.application; import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; +import com.webeye.backend.explanation.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; @@ -16,7 +17,7 @@ public PointExplanationResponse analyzeProductPoint(ProductAnalysisRequest reque return openAiClient.explainProductPoint(request); } - public DetailExplanationResponse analyzeProductDetail(ProductAnalysisRequest request) { + public DetailExplanationResponse analyzeProductDetail(ProductDetailAnalysisRequest request) { return openAiClient.explainProductDetail(request); } } diff --git a/src/main/java/com/webeye/backend/explanation/dto/request/ProductDetailAnalysisRequest.java b/src/main/java/com/webeye/backend/explanation/dto/request/ProductDetailAnalysisRequest.java new file mode 100644 index 0000000..2dd6726 --- /dev/null +++ b/src/main/java/com/webeye/backend/explanation/dto/request/ProductDetailAnalysisRequest.java @@ -0,0 +1,22 @@ +package com.webeye.backend.explanation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; + +import java.util.List; + +@Schema(description = "제품 상세 분석") +@Builder +public record ProductDetailAnalysisRequest( + @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") + String productId, + + @NotEmpty(message = "제품 설명 요소는 비어있을 수 없습니다.") + String description, + + @Schema(description = "상품 이미지 URL") + @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") + List urls +) { +} diff --git a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java index 281d64e..c77d860 100644 --- a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java +++ b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java @@ -2,6 +2,7 @@ import com.webeye.backend.explanation.application.ExplanationService; import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; +import com.webeye.backend.explanation.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.explanation.presentation.swagger.ExplanationSwagger; @@ -30,7 +31,7 @@ public SuccessResponse productAnalysis(@Valid @Request @Override @ResponseStatus(HttpStatus.OK) @PostMapping(value = "/detail") - public SuccessResponse productDetailAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { + public SuccessResponse productDetailAnalysis(@Valid @RequestBody ProductDetailAnalysisRequest request) { return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, explanationService.analyzeProductDetail(request)); } } diff --git a/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java b/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java index 29c6b93..0d7815b 100644 --- a/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java +++ b/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java @@ -1,6 +1,7 @@ package com.webeye.backend.explanation.presentation.swagger; import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; +import com.webeye.backend.explanation.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.global.dto.response.SuccessResponse; @@ -37,7 +38,7 @@ SuccessResponse productAnalysis( ) }) SuccessResponse productDetailAnalysis( - @RequestBody ProductAnalysisRequest request + @RequestBody ProductDetailAnalysisRequest request ); } diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 53a8399..717d7b0 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -3,6 +3,7 @@ import com.webeye.backend.allergy.dto.response.AllergyAiResponse; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; +import com.webeye.backend.explanation.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; import com.webeye.backend.explanation.dto.response.PointExplanationResponse; import com.webeye.backend.global.error.BusinessException; @@ -54,18 +55,18 @@ public PointExplanationResponse explainProductPoint(ProductAnalysisRequest reque } - public DetailExplanationResponse explainProductDetail(ProductAnalysisRequest request) { + public DetailExplanationResponse explainProductDetail(ProductDetailAnalysisRequest request) { String system = """ You are an expert in providing detailed explanations about products based on images. When a user provides a product description image along with the key elements of that description, you should offer a clear and detailed explanation of that element. In this explanation, you must provide very detailed information about that element from the image. """; - String user = """ + String user = String.format(""" Key descriptive element: %s I have provided a product description image along with the key descriptive elements extracted from the image. Please generate a detailed explanation of the provided key descriptive element. - """; + """, request.description()); ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); From be10144e77176d929c4cd6a6659ab1b71b0b6865 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 12 May 2025 23:43:55 +0900 Subject: [PATCH 146/309] =?UTF-8?q?#37=20refactor:=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=84=A4=EB=AA=85=20-=20=ED=95=9C?= =?UTF-8?q?=EA=B5=AD=EC=96=B4=20=EC=9D=91=EB=8B=B5=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 717d7b0..67c5fa1 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -41,7 +41,7 @@ public PointExplanationResponse explainProductPoint(ProductAnalysisRequest reque You may adjust the content depending on the product type, but make sure the extracted items are helpful for making a purchase decision. Limit the result to a maximum of 6 items, and keep each one clear and concise. Below is just a guideline — instead of following the content exactly, follow the intent. - + Example (Generate appropriately according to the product) - 핵심 특징 요약 (제품 포인트) - 당류 비교 (10g당 당류 함량 비교) @@ -59,7 +59,7 @@ public DetailExplanationResponse explainProductDetail(ProductDetailAnalysisReque String system = """ You are an expert in providing detailed explanations about products based on images. When a user provides a product description image along with the key elements of that description, you should offer a clear and detailed explanation of that element. - In this explanation, you must provide very detailed information about that element from the image. + In this explanation, you must provide very detailed information about that element from the image. Answer in Korean. """; String user = String.format(""" From 36d25b6981009175693c7de15d6e8d1263494bc6 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Tue, 13 May 2025 00:30:14 +0900 Subject: [PATCH 147/309] =?UTF-8?q?#36=20feat:=20=EC=98=81=EC=96=91?= =?UTF-8?q?=EC=84=B1=EB=B6=84=20=EC=A0=95=EB=B3=B4=20=ED=8F=AC=ED=95=A8=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=ED=95=84=EB=93=9C=20NutritionAiResponse?= =?UTF-8?q?=20=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 1 + .../backend/nutrition/dto/response/NutritionAiResponse.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 67c5fa1..430b4f9 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -109,6 +109,7 @@ public NutritionAiResponse explainNutrition(FoodProductAnalysisRequest request) String user = """ If the attached images contain 'nutrition information', please provide the amount of each nutrient in the format I sent. + If the nutritional information is not included, set the isNutrientIncluded field to false; if it is included, set it to true. """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); diff --git a/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java index cca48c5..003d889 100644 --- a/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java +++ b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java @@ -6,6 +6,8 @@ @Schema(description = "제품 영양 정보 응답") @Builder public record NutritionAiResponse( + @Schema(description = "영양성분 정보 포함 여부") + Boolean isNutrientIncluded, @Schema(description = "나트륨 (mg)", example = "120.5") Double sodium, @Schema(description = "탄수화물 (g)", example = "25.0") From 14527a1675022a02beeaee969f3ed54a1705a548 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 13 May 2025 01:08:59 +0900 Subject: [PATCH 148/309] =?UTF-8?q?#33=20feat:=20ai=20=EB=B6=84=EC=84=9D?= =?UTF-8?q?=20=ED=9B=84=20db=EC=97=90=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HealthFoodService.java | 42 +++++++++++++++++-- .../mapper/ProductHealthFoodMapper.java | 15 +++++++ .../persistence/HealthFoodRepository.java | 2 +- .../presentation/HealthFoodController.java | 2 +- 4 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/ProductHealthFoodMapper.java diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 9956a9c..e5e4620 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -1,5 +1,7 @@ package com.webeye.backend.healthfood.application; +import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.global.error.ErrorCode; import com.webeye.backend.healthfood.domain.HealthFood; import com.webeye.backend.healthfood.domain.HealthFoodKeyword; import com.webeye.backend.healthfood.domain.Keyword; @@ -8,9 +10,14 @@ import com.webeye.backend.healthfood.dto.HealthFoodResponse; import com.webeye.backend.healthfood.infrastructure.client.HealthFoodClient; import com.webeye.backend.healthfood.infrastructure.mapper.HealthFoodMapper; +import com.webeye.backend.healthfood.infrastructure.mapper.ProductHealthFoodMapper; import com.webeye.backend.healthfood.infrastructure.persistence.HealthFoodRepository; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.domain.ProductHealthfood; import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.persistent.ProductHealthFoodRepository; +import com.webeye.backend.product.persistent.ProductRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -33,7 +40,9 @@ public class HealthFoodService { private final OpenAiClient openAiClient; private final HealthFoodClient healthFoodClient; + private final ProductRepository productRepository; private final HealthFoodRepository healthFoodRepository; + private final ProductHealthFoodRepository productHealthFoodRepository; @Transactional public HealthFoodResponse.I2710 callHealthFoodApi() { @@ -50,21 +59,46 @@ public HealthFoodResponse.I2710 callHealthFoodApi() { return HealthFoodMapper.toResponseList(healthFoods, i2710.totalCount()); } + @Transactional + public HealthFoodAiResponse analyzeAndSave(ProductAnalysisRequest request) { + Product product = productRepository.findById(request.productId()) + .orElseGet(() -> productRepository.save( + Product.builder() + .id(request.productId()) + .build() + ) + ); + List itemNames = analyzeHealthFood(request); + + List healthFoods = healthFoodRepository.findByItemNameIn(itemNames); + + saveProductHealthFood(product, healthFoods); + + List types = mapHealthFoodTypes(itemNames); + + return HealthFoodMapper.toResponse(types); + } + @Transactional(readOnly = true) - public HealthFoodAiResponse analyzeHealthFood(ProductAnalysisRequest request) { + public List analyzeHealthFood(ProductAnalysisRequest request) { List ingredients = healthFoodRepository.findAllItemNames(); String extractedText = openAiClient.explainHealthFood(request, ingredients); - List matchedIngredients = Arrays.stream(extractedText.split(",")) + return Arrays.stream(extractedText.split(",")) .map(String::trim) .filter(ingredients::contains) .distinct() .toList(); + } - List types = mapHealthFoodTypes(matchedIngredients); + @Transactional + public void saveProductHealthFood(Product product, List healthFoods) { + List productHealthFoods = healthFoods.stream() + .map(healthFood -> ProductHealthFoodMapper.toEntity(product, healthFood)) + .toList(); - return HealthFoodMapper.toResponse(types); + productHealthFoodRepository.saveAll(productHealthFoods); } private List mapHealthFoodTypes(List ingredients) { diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/ProductHealthFoodMapper.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/ProductHealthFoodMapper.java new file mode 100644 index 0000000..e22bf58 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/ProductHealthFoodMapper.java @@ -0,0 +1,15 @@ +package com.webeye.backend.healthfood.infrastructure.mapper; + +import com.webeye.backend.healthfood.domain.HealthFood; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.domain.ProductHealthfood; + +public class ProductHealthFoodMapper { + + public static ProductHealthfood toEntity(Product product, HealthFood healthFood) { + return ProductHealthfood.builder() + .product(product) + .healthFood(healthFood) + .build(); + } +} diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java index a9d9402..dbc34b7 100644 --- a/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/persistence/HealthFoodRepository.java @@ -10,6 +10,6 @@ public interface HealthFoodRepository extends JpaRepository { @Query("SELECT h.itemName FROM HealthFood h") List findAllItemNames(); - Optional findByItemNameContaining(String itemName); + List findByItemNameIn(List itemNames); } diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java index 77f07e2..c4ed5b3 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java @@ -33,6 +33,6 @@ public SuccessResponse callHealthFoodApi() { @ResponseStatus(HttpStatus.OK) @PostMapping("/keywords") public SuccessResponse analyzeHealthFood(@Valid @RequestBody ProductAnalysisRequest request) { - return SuccessResponse.of(HEALTH_FOOD_ANALYSIS_SUCCESS, healthFoodService.analyzeHealthFood(request)); + return SuccessResponse.of(HEALTH_FOOD_ANALYSIS_SUCCESS, healthFoodService.analyzeAndSave(request)); } } From 5f991615d444675dee9a96cfb51856de04fe51eb Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 13 May 2025 01:10:51 +0900 Subject: [PATCH 149/309] =?UTF-8?q?#33=20minor:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20import=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/product/domain/Product.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 7c1a701..9f8df9d 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -1,7 +1,6 @@ package com.webeye.backend.product.domain; import com.webeye.backend.global.domain.BaseEntity; -import com.webeye.backend.healthfood.domain.HealthFood; import jakarta.persistence.*; import lombok.*; From e4b8b64bcf60269c36e45c8e360d0d98a9d39ef5 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 13 May 2025 02:52:55 +0900 Subject: [PATCH 150/309] =?UTF-8?q?#33=20fix:=20=EA=B1=B4=EA=B0=95?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=8B=9D=ED=92=88=20=EC=9B=90=EB=A3=8C=20ope?= =?UTF-8?q?n=20api=20=ED=98=B8=EC=B6=9C=20=EC=A3=BC=EC=84=9D=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/HealthFoodController.java | 12 +++++----- .../swagger/HealthFoodSwagger.java | 22 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java index b2fbdbd..ae0f91f 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java @@ -22,12 +22,12 @@ public class HealthFoodController implements HealthFoodSwagger { private final HealthFoodService healthFoodService; - @Override - @ResponseStatus(HttpStatus.OK) - @GetMapping - public SuccessResponse callHealthFoodApi() { - return SuccessResponse.of(HEALTH_FOOD_API_SUCCESS, healthFoodService.callHealthFoodApi()); - } +// @Override +// @ResponseStatus(HttpStatus.OK) +// @GetMapping +// public SuccessResponse callHealthFoodApi() { +// return SuccessResponse.of(HEALTH_FOOD_API_SUCCESS, healthFoodService.callHealthFoodApi()); +// } @Override @ResponseStatus(HttpStatus.OK) diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java b/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java index e3e6c39..e8e3176 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java @@ -13,17 +13,17 @@ @Tag(name = "[건강 기능 식품]", description = "건강 기능 식품 관련 API") public interface HealthFoodSwagger { - @Operation( - summary = "건강 기능 식품 OPEN API 호출", - description = "식품 안전 나라 건강 기능 식품 품목 분류 정보 OPEN API를 호출합니다." - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "OPEN API가 성공적으로 호출되었습니다." - ) - }) - SuccessResponse callHealthFoodApi(); +// @Operation( +// summary = "건강 기능 식품 OPEN API 호출", +// description = "식품 안전 나라 건강 기능 식품 품목 분류 정보 OPEN API를 호출합니다." +// ) +// @ApiResponses(value = { +// @ApiResponse( +// responseCode = "200", +// description = "OPEN API가 성공적으로 호출되었습니다." +// ) +// }) +// SuccessResponse callHealthFoodApi(); @Operation( summary = "건강 기능 식품 효능 키워드 분석", From 5cb577f8e528a3d92a1cee0d69c4d0a0b7150f26 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Tue, 13 May 2025 02:57:50 +0900 Subject: [PATCH 151/309] =?UTF-8?q?#36=20feat:=20=EC=9B=90=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=20=EC=98=81=EC=96=91=EC=84=B1=EB=B6=84=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20OpenAI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/OpenAiClient.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 430b4f9..559c55f 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -10,9 +10,14 @@ import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; +import com.webeye.backend.rawmaterial.dto.response.RawMaterialAiResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.converter.BeanOutputConverter; import org.springframework.core.io.UrlResource; import org.springframework.stereotype.Component; @@ -116,6 +121,7 @@ public NutritionAiResponse explainNutrition(FoodProductAnalysisRequest request) return callWithStructuredOutput(request.urls(), prompt, NutritionAiResponse.class); } + public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { String system = """ You are a cosmetic ingredients description assistant. @@ -184,6 +190,30 @@ private String extractFileExtension(String url) { return fileName.substring(dotIndex + 1); } + + public RawMaterialAiResponse explainRawMaterial(FoodProductAnalysisRequest request) { + Message systemMessage = new SystemMessage(""" + 너는 원재료 식품의 이름을 반환하는 어시스턴트이다. + """); + + Message userMessage = new UserMessage(String.format(""" + %s + 다음은 온라인 쇼핑몰의 식품 제목입니다. 불필요한 수식어나 단위를 제거하고, 핵심 식품명만 추출하세요. 부위명(예: 살, 조각, 포, 덩어리 등)은 제거하고, 일반 식재료명을 사용하세요. 가능한 한 짧고 일반적인 명사 형태로 출력하십시오. + (참고로 "고구마순"은 "고구마_줄기" 이다.) + 예시: + 1. 달콤한 꿀고구마, 1박스, 10kg 못난이 (꿀&호박 랜덤발송) → 꿀고구마 + 2. 건나물 말린 토란줄기 토란대 미얀마산 1kg, 1개 → 토란대_줄기 + 3. [수산맥] 수율90%%내외 박달홍게 프리미엄 선주직송, 1박스, 3kg (고급형10미) → 붉은대게 + """, request.title())); + + Prompt prompt = new Prompt(List.of(systemMessage, userMessage)); + String result = chatClient.prompt(prompt).call().content(); + + return RawMaterialAiResponse.builder() + .name(result) + .build(); + } + } From 6872c850463deb207fa44899989b74181b6b2891 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Tue, 13 May 2025 03:00:18 +0900 Subject: [PATCH 152/309] =?UTF-8?q?#36=20feat:=20=EC=9B=90=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9D=ED=92=88=20=EC=98=81=EC=96=91=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/RawMaterialService.java | 75 +++++++++++++++++++ .../dto/response/RawMaterialAiResponse.java | 12 +++ .../persistent/RawMaterialRepository.java | 12 +++ 3 files changed, 99 insertions(+) create mode 100644 src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java create mode 100644 src/main/java/com/webeye/backend/rawmaterial/dto/response/RawMaterialAiResponse.java create mode 100644 src/main/java/com/webeye/backend/rawmaterial/persistent/RawMaterialRepository.java diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java new file mode 100644 index 0000000..208c2d7 --- /dev/null +++ b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java @@ -0,0 +1,75 @@ +package com.webeye.backend.rawmaterial.application; + +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.nutrition.domain.Nutrient; +import com.webeye.backend.nutrition.domain.type.NutrientType; +import com.webeye.backend.nutrition.persistent.NutrientRepository; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.domain.ProductNutrient; +import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; +import com.webeye.backend.product.persistent.ProductRepository; +import com.webeye.backend.rawmaterial.domain.RawMaterial; +import com.webeye.backend.rawmaterial.persistent.RawMaterialRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RawMaterialService { + private final OpenAiClient openAiClient; + + private final RawMaterialRepository rawMaterialRepository; + private final NutrientRepository nutrientRepository; + private final ProductRepository productRepository; + + @Transactional + public void saveRawMaterialNutrition(Product product, FoodProductAnalysisRequest request) { + String foodName = openAiClient.explainRawMaterial(request).name(); + Optional rawMaterialOpt = rawMaterialRepository.findFirstByNameContaining(foodName); + + rawMaterialOpt.ifPresent(rawMaterial -> { + Map nutrientMap = convertToNutrientMap(rawMaterial); + + nutrientMap.forEach((type, amount) -> { + if (amount == null) return; + Nutrient nutrient = findByType(type); + product.addNutrient( + ProductNutrient.builder() + .product(product) + .nutrient(nutrient) + .amount(amount) + .build() + ); + }); + + productRepository.save(product); + }); + } + + private Map convertToNutrientMap(RawMaterial rawMaterial) { + Map map = new EnumMap<>(NutrientType.class); + map.put(NutrientType.SODIUM, rawMaterial.getSodium()); + map.put(NutrientType.CARBOHYDRATE, rawMaterial.getCarbohydrate()); + map.put(NutrientType.SUGARS, rawMaterial.getSugars()); + map.put(NutrientType.FAT, rawMaterial.getFat()); + map.put(NutrientType.TRANS_FAT, rawMaterial.getTransFat()); + map.put(NutrientType.SATURATED_FAT, rawMaterial.getSaturatedFat()); + map.put(NutrientType.CHOLESTEROL, rawMaterial.getCholesterol()); + map.put(NutrientType.PROTEIN, rawMaterial.getProtein()); + map.put(NutrientType.CALCIUM, rawMaterial.getCalcium()); + map.put(NutrientType.PHOSPHORUS, rawMaterial.getPhosphorus()); + map.put(NutrientType.NIACIN, rawMaterial.getNiacin()); + return map; + } + + private Nutrient findByType(NutrientType type) { + return nutrientRepository.findByType(type) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 영양소 타입: " + type)); + } +} diff --git a/src/main/java/com/webeye/backend/rawmaterial/dto/response/RawMaterialAiResponse.java b/src/main/java/com/webeye/backend/rawmaterial/dto/response/RawMaterialAiResponse.java new file mode 100644 index 0000000..f238ee9 --- /dev/null +++ b/src/main/java/com/webeye/backend/rawmaterial/dto/response/RawMaterialAiResponse.java @@ -0,0 +1,12 @@ +package com.webeye.backend.rawmaterial.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema(description = "원재료 식품 AI 응답") +@Builder +public record RawMaterialAiResponse( + @Schema(description = "원재료 식품 이름") + String name +) { +} diff --git a/src/main/java/com/webeye/backend/rawmaterial/persistent/RawMaterialRepository.java b/src/main/java/com/webeye/backend/rawmaterial/persistent/RawMaterialRepository.java new file mode 100644 index 0000000..1be7f16 --- /dev/null +++ b/src/main/java/com/webeye/backend/rawmaterial/persistent/RawMaterialRepository.java @@ -0,0 +1,12 @@ +package com.webeye.backend.rawmaterial.persistent; + +import com.webeye.backend.rawmaterial.domain.RawMaterial; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RawMaterialRepository extends JpaRepository { + Optional findFirstByNameContaining(String name); +} From 15a041a8e402a50b3e9ea1d266ae8bddeb1a83b8 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Tue, 13 May 2025 03:02:39 +0900 Subject: [PATCH 153/309] =?UTF-8?q?#36=20feat:=20=EC=9B=90=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=20=EC=98=81=EC=96=91=EC=84=B1=EB=B6=84=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/NutritionService.java | 34 +++++++++++-------- .../request/FoodProductAnalysisRequest.java | 10 ++++-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index f5d99b5..768e249 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -11,6 +11,7 @@ import com.webeye.backend.product.domain.Product; import com.webeye.backend.product.domain.ProductNutrient; import com.webeye.backend.product.persistent.ProductRepository; +import com.webeye.backend.rawmaterial.application.RawMaterialService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,6 +24,7 @@ @Transactional(readOnly = true) public class NutritionService { private final OpenAiClient openAiClient; + private final RawMaterialService rawMaterialService; private final NutrientRepository nutrientRepository; private final ProductRepository productRepository; @@ -38,21 +40,25 @@ public Nutrient findByType(NutrientType type) { @Transactional public void saveProductNutrition(Product product, FoodProductAnalysisRequest request) { - NutritionAiResponse response = analyzeNutrition(request); - Map nutrientMap = extractNutrientMap(response); + NutritionAiResponse response = openAiClient.explainNutrition(request); + if (response.isNutrientIncluded()) { + Map nutrientMap = extractNutrientMap(response); - nutrientMap.forEach((type, amount) -> { - if (amount == null) return; - Nutrient nutrient = findByType(type); - product.addNutrient( - ProductNutrient.builder() - .product(product) - .nutrient(nutrient) - .amount(amount) - .build() - ); - }); - productRepository.save(product); + nutrientMap.forEach((type, amount) -> { + if (amount == null) return; + Nutrient nutrient = findByType(type); + product.addNutrient( + ProductNutrient.builder() + .product(product) + .nutrient(nutrient) + .amount(amount) + .build() + ); + }); + productRepository.save(product); + return; + } + rawMaterialService.saveRawMaterialNutrition(product, request); } private Map extractNutrientMap(NutritionAiResponse response) { diff --git a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java index f7e6c51..0de8c0b 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java @@ -4,6 +4,7 @@ import com.webeye.backend.nutrition.domain.type.Gender; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import java.util.List; @@ -14,16 +15,19 @@ public record FoodProductAnalysisRequest( @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") String productId, + @Schema(description = "상품 제목") + String title, + @Schema(description = "상품 이미지 URL") @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") List urls, @Schema(description = "사용자 출생년도") - @NotEmpty(message = "사용자의 출생연도는 비어있을 수 없습니다.") - int birthYear, + @NotNull(message = "사용자의 출생연도는 비어있을 수 없습니다.") + Integer birthYear, @Schema(description = "사용자 성별") - @NotEmpty(message = "사용자의 성별은 비어있을 수 없습니다.") + @NotNull(message = "사용자의 성별은 비어있을 수 없습니다.") Gender gender, @Schema(description = "사용자 알레르기") From a59ae28ca1135a0f8020e386d698cbcca9c9fe25 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 13 May 2025 03:05:43 +0900 Subject: [PATCH 154/309] =?UTF-8?q?#33=20fix:=20=EC=9B=90=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=EB=AA=85=20100=EA=B0=9C=EC=94=A9=20=EB=82=98=EB=88=84?= =?UTF-8?q?=EC=96=B4=20=EC=A0=84=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HealthFoodService.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 09372f3..06d3157 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -1,7 +1,5 @@ package com.webeye.backend.healthfood.application; -import com.webeye.backend.global.error.BusinessException; -import com.webeye.backend.global.error.ErrorCode; import com.webeye.backend.healthfood.domain.HealthFood; import com.webeye.backend.healthfood.domain.HealthFoodKeyword; import com.webeye.backend.healthfood.domain.Keyword; @@ -24,6 +22,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -83,13 +82,23 @@ public HealthFoodAiResponse analyzeAndSave(FoodProductAnalysisRequest request) { public List analyzeHealthFood(FoodProductAnalysisRequest request) { List ingredients = healthFoodRepository.findAllItemNames(); - String extractedText = openAiClient.explainHealthFood(request, ingredients); + List matchedIngredients = new ArrayList<>(); - return Arrays.stream(extractedText.split(",")) - .map(String::trim) - .filter(ingredients::contains) - .distinct() - .toList(); + // 100개씩 나누어 전송 + for (int i = 0; i < ingredients.size(); i += 100) { + List batch = ingredients.subList(i, Math.min(i + 100, ingredients.size())); + + String extractedText = openAiClient.explainHealthFood(request, ingredients); + + List matched = Arrays.stream(extractedText.split(",")) + .map(String::trim) + .filter(batch::contains) + .distinct() + .toList(); + + matchedIngredients.addAll(matched); + } + return matchedIngredients.stream().distinct().toList(); } @Transactional From 959f251166559831ea6f46add15619edabe9baf6 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Tue, 13 May 2025 03:07:33 +0900 Subject: [PATCH 155/309] #36 refactor: primitive boolean expression --- .../webeye/backend/nutrition/application/NutritionService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index 768e249..9f83c79 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -41,7 +41,7 @@ public Nutrient findByType(NutrientType type) { @Transactional public void saveProductNutrition(Product product, FoodProductAnalysisRequest request) { NutritionAiResponse response = openAiClient.explainNutrition(request); - if (response.isNutrientIncluded()) { + if (Boolean.TRUE.equals(response.isNutrientIncluded())) { Map nutrientMap = extractNutrientMap(response); nutrientMap.forEach((type, amount) -> { From 4d855bc93b41ecc051446e94534fab516f06dc58 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 13 May 2025 03:13:53 +0900 Subject: [PATCH 156/309] =?UTF-8?q?#33=20chore:=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EB=90=9C=20=EC=A0=9C=ED=92=88-=EA=B1=B4=EA=B0=95=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=8B=9D=ED=92=88=20=ED=82=A4=EC=9B=8C=EB=93=9C=20tod?= =?UTF-8?q?o=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/healthfood/application/HealthFoodService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 06d3157..99eec55 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -110,6 +110,8 @@ public void saveProductHealthFood(Product product, List healthFoods) productHealthFoodRepository.saveAll(productHealthFoods); } + // TODO: 저장된 제품 - 건강기능식품 키워드 조회 + private List mapHealthFoodTypes(List ingredients) { return ingredients.stream() .flatMap(ingredient -> healthFoodRepository.findByItemNameContaining(ingredient).stream()) From 15e534d1ea61add4dc4909732da2af63e17fe150 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 13 May 2025 03:42:33 +0900 Subject: [PATCH 157/309] =?UTF-8?q?#40=20fix:=20webConfig=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 13ea0b2..890e308 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -10,7 +10,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "https://voim.store") + .allowedOrigins("http://localhost:3000", "https://voim.store", "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm") .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); From 418540fb7c5a15e76dcd72522d1e47da27e9c7f4 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 13 May 2025 03:54:06 +0900 Subject: [PATCH 158/309] =?UTF-8?q?#33=20fix:=20coderabbit=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../healthfood/application/HealthFoodService.java | 9 ++++----- .../healthfood/presentation/HealthFoodController.java | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 99eec55..40333a2 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -73,7 +73,7 @@ public HealthFoodAiResponse analyzeAndSave(FoodProductAnalysisRequest request) { saveProductHealthFood(product, healthFoods); - List types = mapHealthFoodTypes(itemNames); + List types = mapHealthFoodTypes(healthFoods); return HealthFoodMapper.toResponse(types); } @@ -88,7 +88,7 @@ public List analyzeHealthFood(FoodProductAnalysisRequest request) { for (int i = 0; i < ingredients.size(); i += 100) { List batch = ingredients.subList(i, Math.min(i + 100, ingredients.size())); - String extractedText = openAiClient.explainHealthFood(request, ingredients); + String extractedText = openAiClient.explainHealthFood(request, batch); List matched = Arrays.stream(extractedText.split(",")) .map(String::trim) @@ -112,9 +112,8 @@ public void saveProductHealthFood(Product product, List healthFoods) // TODO: 저장된 제품 - 건강기능식품 키워드 조회 - private List mapHealthFoodTypes(List ingredients) { - return ingredients.stream() - .flatMap(ingredient -> healthFoodRepository.findByItemNameContaining(ingredient).stream()) + private List mapHealthFoodTypes(List healthFoods) { + return healthFoods.stream() .flatMap(healthFood -> healthFood.getHealthFoodKeywords().stream()) .map(HealthFoodKeyword::getKeyword) .map(Keyword::getType) diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java index ae0f91f..7fe3166 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java @@ -6,7 +6,6 @@ import com.webeye.backend.healthfood.dto.HealthFoodResponse; import com.webeye.backend.healthfood.presentation.swagger.HealthFoodSwagger; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; -import io.swagger.v3.oas.annotations.parameters.RequestBody; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; From 962c2bfd52147e4ed07823b3e925e42c6ccd5e37 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 13 May 2025 20:58:02 +0900 Subject: [PATCH 159/309] =?UTF-8?q?#42=20fix:=20=ED=99=94=EC=9E=A5?= =?UTF-8?q?=ED=92=88=20=EC=A3=BC=EC=9D=98=20=EC=84=B1=EB=B6=84=20response?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/CosmeticResponse.java | 91 +++++++++++++------ 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java b/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java index 7d9f2d4..80bd107 100644 --- a/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java +++ b/src/main/java/com/webeye/backend/cosmetic/dto/response/CosmeticResponse.java @@ -1,38 +1,69 @@ package com.webeye.backend.cosmetic.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; @Schema(description = "화장품 주의 성분") +@Builder public record CosmeticResponse( - @Schema(description = "아밀신남알") - boolean amylCinnamal, - @Schema(description = "벤질알코올") - boolean benzylAlcohol, - @Schema(description = "신나밀알코올") - boolean cinnamylAlcohol, - @Schema(description = "시트랄") - boolean citral, - @Schema(description = "유제놀") - boolean eugenol, - @Schema(description = "하이드록시시트로넬알") - boolean hydroxycitronellal, - @Schema(description = "이소유제놀") - boolean isoeugenol, - @Schema(description = "아밀신나밀알코올") - boolean amylCinnamylAlcohol, - @Schema(description = "벤질살리실레이트") - boolean benzylSalicylate, - @Schema(description = "신남알") - boolean cinnamal, - @Schema(description = "쿠마린") - boolean coumarin, - @Schema(description = "제라니올") - boolean geraniol, - @Schema(description = "하이드록시이소헥실3-사이클로헥센카복스알데하이드") - boolean hicc, - @Schema(description = "아니스에탄올") - boolean anisylAlcohol, - @Schema(description = "벤질신나메이트") - boolean benzylCinnamate + @Schema(description = "아보벤존 (Avobenzone)") + boolean avobenzone, + + @Schema(description = "이소프로필 알코올 (Isopropyl Alcohol)") + boolean isopropylAlcohol, + + @Schema(description = "소듐 라우릴/라우레스 설페이트 (SLS, SLES)") + boolean sodiumLaurylSulfate, + + @Schema(description = "트리에탄올아민 (Triethanolamine, TEA)") + boolean triethanolamine, + + @Schema(description = "폴리에틸렌 글라이콜 (Polyethylene Glycol, PEGs)") + boolean polyethyleneGlycol, + + @Schema(description = "합성 착색료") + boolean syntheticColorant, + + @Schema(description = "이소프로필 메틸페놀 (Isopropyl Methylphenol, IPMP)") + boolean isopropylMethylphenol, + + @Schema(description = "소르빅 애씨드 (Sorbic Acid)") + boolean sorbicAcid, + + @Schema(description = "호르몬류") + boolean hormone, + + @Schema(description = "디부틸 하이드록시 톨루엔 (Dibutyl Hydroxy Toluene, BHT)") + boolean dibutylHydroxyToluene, + + @Schema(description = "파라벤류 (Methyl-, Ethyl-, Propylparaben 등)") + boolean parabens, + + @Schema(description = "트리클로산 (Triclosan)") + boolean triclosan, + + @Schema(description = "부틸 하이드록시아니솔 (Butylated Hydroxyanisole, BHA)") + boolean butylatedHydroxyanisole, + + @Schema(description = "옥시벤존 (Oxybenzone, Benzophenone-3)") + boolean oxybenzone, + + @Schema(description = "포름알데히드 유도체 (Imidazolidinyl Urea, Diazolidinyl Urea, DMDM Hydantoin 등)") + boolean imidazolidinylUrea, + + @Schema(description = "미네랄 오일 (Mineral Oil, Liquid Paraffin 등)") + boolean mineralOil, + + @Schema(description = "티몰 (Thymol)") + boolean thymol, + + @Schema(description = "트라이아이소프로판올아민 (Triisopropanolamine)") + boolean triisopropanolamine, + + @Schema(description = "인공 향료 (Synthetic Fragrance, Parfum)") + boolean syntheticFragrance, + + @Schema(description = "페녹시에탄올 (Phenoxyethanol)") + boolean phenoxyethanol ) { } From 7e231a3340c11fb876aa4d7a91ce0523b634ff93 Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 14 May 2025 00:24:26 +0900 Subject: [PATCH 160/309] =?UTF-8?q?#40=20feat:=20origin=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/webeye/backend/global/config/WebConfig.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 890e308..ddb14d5 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -10,7 +10,13 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "https://voim.store", "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm") + .allowedOrigins( + "https://voim.store", + "http://localhost:3000", + "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm", + "chrome-extension://jeppkpjgeheckphiogogbffdenhlkclh", + "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf" + ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); From ab2ee9da743ee9bb005c807f520fa0e08b4a9f8c Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 14 May 2025 23:16:46 +0900 Subject: [PATCH 161/309] =?UTF-8?q?#33=20fix:=20String=20->=20List=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HealthFoodService.java | 35 +++--------- .../healthfood/dto/HealthFoodAiResponse.java | 5 +- .../dto/HealthFoodKeywordResponse.java | 14 +++++ .../mapper/HealthFoodMapper.java | 8 +-- .../presentation/HealthFoodController.java | 5 +- .../swagger/HealthFoodSwagger.java | 5 +- .../infrastructure/OpenAiClient.java | 56 ++++++++----------- 7 files changed, 54 insertions(+), 74 deletions(-) create mode 100644 src/main/java/com/webeye/backend/healthfood/dto/HealthFoodKeywordResponse.java diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 40333a2..b9a27ee 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -5,6 +5,7 @@ import com.webeye.backend.healthfood.domain.Keyword; import com.webeye.backend.healthfood.domain.type.HealthFoodType; import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; +import com.webeye.backend.healthfood.dto.HealthFoodKeywordResponse; import com.webeye.backend.healthfood.dto.HealthFoodResponse; import com.webeye.backend.healthfood.infrastructure.client.HealthFoodClient; import com.webeye.backend.healthfood.infrastructure.mapper.HealthFoodMapper; @@ -22,8 +23,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; @Slf4j @@ -59,17 +58,16 @@ public HealthFoodResponse.I2710 callHealthFoodApi() { } @Transactional - public HealthFoodAiResponse analyzeAndSave(FoodProductAnalysisRequest request) { + public HealthFoodKeywordResponse analyzeAndSave(FoodProductAnalysisRequest request) { Product product = productRepository.findById(request.productId()) .orElseGet(() -> productRepository.save( Product.builder() .id(request.productId()) .build() - ) - ); - List itemNames = analyzeHealthFood(request); + )); + HealthFoodAiResponse response = analyzeHealthFood(request); - List healthFoods = healthFoodRepository.findByItemNameIn(itemNames); + List healthFoods = healthFoodRepository.findByItemNameIn(response.itemNames()); saveProductHealthFood(product, healthFoods); @@ -79,26 +77,9 @@ public HealthFoodAiResponse analyzeAndSave(FoodProductAnalysisRequest request) { } @Transactional(readOnly = true) - public List analyzeHealthFood(FoodProductAnalysisRequest request) { + public HealthFoodAiResponse analyzeHealthFood(FoodProductAnalysisRequest request) { List ingredients = healthFoodRepository.findAllItemNames(); - - List matchedIngredients = new ArrayList<>(); - - // 100개씩 나누어 전송 - for (int i = 0; i < ingredients.size(); i += 100) { - List batch = ingredients.subList(i, Math.min(i + 100, ingredients.size())); - - String extractedText = openAiClient.explainHealthFood(request, batch); - - List matched = Arrays.stream(extractedText.split(",")) - .map(String::trim) - .filter(batch::contains) - .distinct() - .toList(); - - matchedIngredients.addAll(matched); - } - return matchedIngredients.stream().distinct().toList(); + return openAiClient.explainHealthFood(request, ingredients); } @Transactional @@ -110,8 +91,6 @@ public void saveProductHealthFood(Product product, List healthFoods) productHealthFoodRepository.saveAll(productHealthFoods); } - // TODO: 저장된 제품 - 건강기능식품 키워드 조회 - private List mapHealthFoodTypes(List healthFoods) { return healthFoods.stream() .flatMap(healthFood -> healthFood.getHealthFoodKeywords().stream()) diff --git a/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java index 74996d0..893ba87 100644 --- a/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java +++ b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodAiResponse.java @@ -1,14 +1,13 @@ package com.webeye.backend.healthfood.dto; -import com.webeye.backend.healthfood.domain.type.HealthFoodType; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import java.util.List; @Builder -@Schema(description = "건강 기능 식품 분석") +@Schema(description = "건강 기능 식품 원료명 추출") public record HealthFoodAiResponse( - List healthFoodTypes + List itemNames ) { } diff --git a/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodKeywordResponse.java b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodKeywordResponse.java new file mode 100644 index 0000000..a510789 --- /dev/null +++ b/src/main/java/com/webeye/backend/healthfood/dto/HealthFoodKeywordResponse.java @@ -0,0 +1,14 @@ +package com.webeye.backend.healthfood.dto; + +import com.webeye.backend.healthfood.domain.type.HealthFoodType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.util.List; + +@Builder +@Schema(description = "건강 기능 식품 키워드 분석") +public record HealthFoodKeywordResponse( + List types +) { +} diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java index 2ea070c..b2967ed 100644 --- a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java @@ -2,7 +2,7 @@ import com.webeye.backend.healthfood.domain.HealthFood; import com.webeye.backend.healthfood.domain.type.HealthFoodType; -import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; +import com.webeye.backend.healthfood.dto.HealthFoodKeywordResponse; import com.webeye.backend.healthfood.dto.HealthFoodResponse; import java.util.List; @@ -38,9 +38,9 @@ public static HealthFoodResponse.I2710 toResponseList(List healthFoo return new HealthFoodResponse.I2710(totalCount, i2710List); } - public static HealthFoodAiResponse toResponse(List types) { - return HealthFoodAiResponse.builder() - .healthFoodTypes(types) + public static HealthFoodKeywordResponse toResponse(List types) { + return HealthFoodKeywordResponse.builder() + .types(types) .build(); } } diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java index 7fe3166..d12dfed 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java @@ -2,8 +2,7 @@ import com.webeye.backend.global.dto.response.SuccessResponse; import com.webeye.backend.healthfood.application.HealthFoodService; -import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; -import com.webeye.backend.healthfood.dto.HealthFoodResponse; +import com.webeye.backend.healthfood.dto.HealthFoodKeywordResponse; import com.webeye.backend.healthfood.presentation.swagger.HealthFoodSwagger; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import jakarta.validation.Valid; @@ -31,7 +30,7 @@ public class HealthFoodController implements HealthFoodSwagger { @Override @ResponseStatus(HttpStatus.OK) @PostMapping("/keywords") - public SuccessResponse analyzeHealthFood(@Valid @RequestBody FoodProductAnalysisRequest request) { + public SuccessResponse analyzeHealthFood(@Valid @RequestBody FoodProductAnalysisRequest request) { return SuccessResponse.of(HEALTH_FOOD_ANALYSIS_SUCCESS, healthFoodService.analyzeAndSave(request)); } } diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java b/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java index e8e3176..3d40e10 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/swagger/HealthFoodSwagger.java @@ -1,8 +1,7 @@ package com.webeye.backend.healthfood.presentation.swagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; -import com.webeye.backend.healthfood.dto.HealthFoodResponse; +import com.webeye.backend.healthfood.dto.HealthFoodKeywordResponse; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -35,7 +34,7 @@ public interface HealthFoodSwagger { description = "건강 기능 식품 효능 키워드 분석이 성공적으로 실행되었습니다." ) }) - SuccessResponse analyzeHealthFood( + SuccessResponse analyzeHealthFood( @Valid @RequestBody FoodProductAnalysisRequest request ); } diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index d7bc0c4..e9b79c0 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -152,27 +152,38 @@ public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { return callWithStructuredOutput(request.urls(), prompt, CosmeticResponse.class); } - public String explainHealthFood(FoodProductAnalysisRequest request, List ingredients) { + public HealthFoodAiResponse explainHealthFood(FoodProductAnalysisRequest request, List ingredients) { String system = """ - You are a health food ingredient analysis expert. - You specialize in identifying functional health food ingredients based on product labels or ingredient lists. - Do not include explanations, summaries, or any other text. + You are a health food ingredient analysis expert specializing in Korean product label interpretation. + Your role is to analyze ingredient labels and identify whether any functional health food ingredients are present. + Only extract ingredient names — do not summarize, explain, or add any extra commentary. """; String user = String.format(""" - If the attached images contain 'nutrition information', please provide the amount of each ingredient in the format I sent. + Step 1: Analyze the attached product label image(s). - Check if any of the following ingredients are present: - %s + Step 2: Focus only on the section labeled as '원재료명', '원재료 및 함량', or other similar terms indicating the ingredient list. + + Step 3: From the identified ingredient section, extract only the ingredient names. Do not extract amounts, units(e.g. mg, %%), or unnecessary modifiers. + + Step 4: Compare the extracted ingredient names to the following list of known functional health food ingredients: + %s + + Step 5: Match ingredients based on the following rules: + - Exact matches (e.g., '비타민C' → '비타민C') + - Partial matches embedded in compound terms (e.g., '베타카로틴함유' → match '베타카로틴', '비타민C분말' → match '비타민C') + - Ignore matches that are too vague or semantically unrelated. + + Example (Generate appropriately according to the product): + - 베타카로틴 + - 비타민C + """, String.join(", ", ingredients)); - If any are found, list only the matched ingredient names separated by commas. - If none are found, reply with "None". - """, String.join(", ", ingredients) - ); ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithRawOutput(request, prompt); + return callWithStructuredOutput(request.urls(), prompt, HealthFoodAiResponse.class); } + private T callWithStructuredOutput(List urls, ImageAnalysisPrompt prompt, Class clazz) { BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); @@ -196,27 +207,6 @@ private T callWithStructuredOutput(List urls, ImageAnalysisPrompt pr return outputConverter.convert(response); } - private String callWithRawOutput(FoodProductAnalysisRequest request, ImageAnalysisPrompt prompt) { - String response = chatClient.prompt() - .user(promptUserSpec -> { - try { - promptUserSpec.text(prompt.user()); - for (String imageUrl : request.urls()) { - MimeType extension = ImageMimeType.fromExtension(extractFileExtension(imageUrl)); - promptUserSpec.media(extension, new UrlResource(imageUrl)); - } - } catch (MalformedURLException exception) { - log.error("MalformedURLException: callWithStructuredOutput() 에서 발생"); - throw new BusinessException(INVALID_IMAGE_URL); - } - }) - .system(prompt.system()) - .call() - .content(); - - return response; - } - private String extractFileExtension(String url) { String fileName = url.substring(url.lastIndexOf('/') + 1); int dotIndex = fileName.lastIndexOf('.'); From 3e7ef46f3af8046506d53109262c70f139c64a13 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 15 May 2025 00:17:43 +0900 Subject: [PATCH 162/309] =?UTF-8?q?#33=20fix:=20open=20ai=20=EC=9B=90?= =?UTF-8?q?=EB=A3=8C=EB=AA=85=20=EC=B6=94=EC=B6=9C=20->=20db=20=ED=83=90?= =?UTF-8?q?=EC=83=89=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HealthFoodService.java | 19 ++++++-- .../presentation/HealthFoodController.java | 2 +- .../infrastructure/OpenAiClient.java | 48 ++++++++++--------- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index b9a27ee..27298f6 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -58,7 +58,7 @@ public HealthFoodResponse.I2710 callHealthFoodApi() { } @Transactional - public HealthFoodKeywordResponse analyzeAndSave(FoodProductAnalysisRequest request) { + public HealthFoodKeywordResponse analyzeAndSaveHealthFood(FoodProductAnalysisRequest request) { Product product = productRepository.findById(request.productId()) .orElseGet(() -> productRepository.save( Product.builder() @@ -67,7 +67,10 @@ public HealthFoodKeywordResponse analyzeAndSave(FoodProductAnalysisRequest reque )); HealthFoodAiResponse response = analyzeHealthFood(request); - List healthFoods = healthFoodRepository.findByItemNameIn(response.itemNames()); + List dbItemNames = healthFoodRepository.findAllItemNames(); + List itemNames = matchItemNames(response.itemNames(), dbItemNames); + + List healthFoods = healthFoodRepository.findByItemNameIn(itemNames); saveProductHealthFood(product, healthFoods); @@ -76,10 +79,8 @@ public HealthFoodKeywordResponse analyzeAndSave(FoodProductAnalysisRequest reque return HealthFoodMapper.toResponse(types); } - @Transactional(readOnly = true) public HealthFoodAiResponse analyzeHealthFood(FoodProductAnalysisRequest request) { - List ingredients = healthFoodRepository.findAllItemNames(); - return openAiClient.explainHealthFood(request, ingredients); + return openAiClient.explainHealthFood(request); } @Transactional @@ -99,4 +100,12 @@ private List mapHealthFoodTypes(List healthFoods) { .distinct() .toList(); } + + private List matchItemNames(List aiItemNames, List dbItemNames) { + return aiItemNames.stream() + .flatMap(ai -> dbItemNames.stream() + .filter(ai::contains)) + .distinct() + .toList(); + } } diff --git a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java index d12dfed..60475ee 100644 --- a/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java +++ b/src/main/java/com/webeye/backend/healthfood/presentation/HealthFoodController.java @@ -31,6 +31,6 @@ public class HealthFoodController implements HealthFoodSwagger { @ResponseStatus(HttpStatus.OK) @PostMapping("/keywords") public SuccessResponse analyzeHealthFood(@Valid @RequestBody FoodProductAnalysisRequest request) { - return SuccessResponse.of(HEALTH_FOOD_ANALYSIS_SUCCESS, healthFoodService.analyzeAndSave(request)); + return SuccessResponse.of(HEALTH_FOOD_ANALYSIS_SUCCESS, healthFoodService.analyzeAndSaveHealthFood(request)); } } diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index e9b79c0..0e390f1 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -152,38 +152,42 @@ public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { return callWithStructuredOutput(request.urls(), prompt, CosmeticResponse.class); } - public HealthFoodAiResponse explainHealthFood(FoodProductAnalysisRequest request, List ingredients) { + public HealthFoodAiResponse explainHealthFood(FoodProductAnalysisRequest request) { String system = """ - You are a health food ingredient analysis expert specializing in Korean product label interpretation. - Your role is to analyze ingredient labels and identify whether any functional health food ingredients are present. - Only extract ingredient names — do not summarize, explain, or add any extra commentary. + You are a food label OCR expert. Your task is to extract ingredient names from Korean health supplement product images. + You must return only a list of ingredient names, in JSON format. Do not summarize or explain anything. """; - String user = String.format(""" - Step 1: Analyze the attached product label image(s). - - Step 2: Focus only on the section labeled as '원재료명', '원재료 및 함량', or other similar terms indicating the ingredient list. - - Step 3: From the identified ingredient section, extract only the ingredient names. Do not extract amounts, units(e.g. mg, %%), or unnecessary modifiers. - - Step 4: Compare the extracted ingredient names to the following list of known functional health food ingredients: - %s + String user = """ + Please examine the attached image of a Korean health food product. - Step 5: Match ingredients based on the following rules: - - Exact matches (e.g., '비타민C' → '비타민C') - - Partial matches embedded in compound terms (e.g., '베타카로틴함유' → match '베타카로틴', '비타민C분말' → match '비타민C') - - Ignore matches that are too vague or semantically unrelated. + Look for the section that lists ingredients. This section is usually labeled with: + - '원재료명' + - '원재료 및 함량' + - or similar titles + + From this section, extract only the ingredient names. For example: + - "비타민C 100mg" → "비타민C" + - "베타카로틴함유" → "베타카로틴" + - "정제수(물)" → "정제수" + + Ignore quantities (mg, %, g), descriptors (함유, 분말), or anything in parentheses + + Return only the names of the ingredients in the following JSON format: + { + "itemNames": ["비타민C", "베타카로틴", "정제수"] + } - Example (Generate appropriately according to the product): - - 베타카로틴 - - 비타민C - """, String.join(", ", ingredients)); + If no ingredients are found, return: + { + "itemNames": [] + } + """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); return callWithStructuredOutput(request.urls(), prompt, HealthFoodAiResponse.class); } - private T callWithStructuredOutput(List urls, ImageAnalysisPrompt prompt, Class clazz) { BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); From 1717b483503ac4c3a981d1cdfb871ce7481302f7 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 15 May 2025 03:14:28 +0900 Subject: [PATCH 163/309] =?UTF-8?q?#36=20remove:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20API=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/NutritionService.java | 4 --- .../presentation/NutritionController.java | 27 ------------------- .../swagger/NutritionSwagger.java | 27 ------------------- 3 files changed, 58 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java delete mode 100644 src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index 9f83c79..af9ea84 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -29,10 +29,6 @@ public class NutritionService { private final NutrientRepository nutrientRepository; private final ProductRepository productRepository; - public NutritionAiResponse analyzeNutrition(FoodProductAnalysisRequest request) { - return openAiClient.explainNutrition(request); - } - public Nutrient findByType(NutrientType type) { return nutrientRepository.findByType(type) .orElseThrow(() -> new BusinessException(ErrorCode.NUTRIENT_NOT_FOUND)); diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java b/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java deleted file mode 100644 index 8287439..0000000 --- a/src/main/java/com/webeye/backend/nutrition/presentation/NutritionController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.webeye.backend.nutrition.presentation; - -import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; -import com.webeye.backend.nutrition.application.NutritionService; -import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; -import com.webeye.backend.nutrition.presentation.swagger.NutritionSwagger; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -import static com.webeye.backend.global.dto.response.type.SuccessCode.NUTRITION_ANALYSIS_SUCCESS; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/v1/nutrition") -public class NutritionController implements NutritionSwagger { - private final NutritionService nutritionService; - - @Override - @ResponseStatus(HttpStatus.OK) - @PostMapping(value = "") - public SuccessResponse nutritionAnalysis(@Valid @RequestBody FoodProductAnalysisRequest request) { - return SuccessResponse.of(NUTRITION_ANALYSIS_SUCCESS, nutritionService.analyzeNutrition(request)); - } -} diff --git a/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java b/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java deleted file mode 100644 index 276e942..0000000 --- a/src/main/java/com/webeye/backend/nutrition/presentation/swagger/NutritionSwagger.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.webeye.backend.nutrition.presentation.swagger; - -import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; -import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.RequestBody; - -@Tag(name = "Nutrition", description = "제품 영양소 관련 API") -public interface NutritionSwagger { - @Operation( - summary = "제품의 영양소 함량 추출", - description = "음식 제품에 대한 영양소 함량을 추출합니다." - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "영양소 함량이 성공적으로 추출되었습니다." - ) - }) - SuccessResponse nutritionAnalysis( - @RequestBody FoodProductAnalysisRequest request - ); -} From c7661625e220b338f1e1065976180890502a5c7f Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 15 May 2025 14:36:50 +0900 Subject: [PATCH 164/309] =?UTF-8?q?#42=20feat:=20explainCosmetic=20?= =?UTF-8?q?=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/OpenAiClient.java | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 0e390f1..dce026d 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -118,34 +118,41 @@ public NutritionAiResponse explainNutrition(FoodProductAnalysisRequest request) public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { String system = """ - You are a cosmetic ingredients description assistant. + You are an expert in identifying harmful cosmetic ingredients based on Korean labels. + You always return exact matches based on a predefined Korean-to-English mapping. """; String user = """ - Check the attached image for cosmetic ingredients. + Carefully examine the attached image. Focus only on Korean ingredient names. + + Your goal is to check for the exact **presence** of the following Korean ingredient names: + (These names must appear exactly and completely in the image, including spacing and punctuation) Use this mapping: { - "아밀신남알": "amylCinnamal", - "벤질알코올": "benzylAlcohol", - "신나밀알코올": "cinnamylAlcohol", - "시트랄": "citral", - "유제놀": "eugenol", - "하이드록시시트로넬알": "hydroxycitronellal", - "이소유제놀": "isoeugenol", - "아밀신나밀알코올": "amylCinnamylAlcohol", - "벤질살리실레이트": "benzylSalicylate", - "신남알": "cinnamal", - "쿠마린": "coumarin", - "제라니올": "geraniol", - "하이드록시이소헥실3-사이클로헥센카복스알데하이드": "hydroxyisohexyl3CyclohexeneCarboxaldehyde", - "아니스에탄올": "anisylAlcohol", - "벤질신나메이트": "benzylCinnamate" + "아보벤존": "avobenzone", + "이소프로필 알코올": "isopropylAlcohol", + "소듐 라우릴/라우레스 설페이트 (SLS, SLES)": "sodiumLaurylSulfate", + "트리에탄올아민": "triethanolamine", + "폴리에틸렌 글라이콜 (PEGs)": "polyethyleneGlycol", + "합성 착색료": "syntheticColorant", + "이소프로필 메틸페놀": "isopropylMethylphenol", + "소르빅 애씨드": "sorbicAcid", + "호르몬류": "hormone", + "디부틸 하이드록시 톨루엔 (BHT)": "dibutylHydroxyToluene", + "파라벤류 (Methyl-, Ethyl-, Propylparaben 등)": "parabens", + "트리클로산": "triclosan", + "부틸 하이드록시아니솔 (BHA)": "butylatedHydroxyanisole", + "옥시벤존": "oxybenzone", + "이미다졸리디닐 우레아, 디아졸리디닐 우레아, DMDM 하이단토인 등": "imidazolidinylUrea", + "미네랄 오일, 파라핀오일": "mineralOil", + "티몰": "thymol", + "트라이아이소프로판올아민": "triisopropanolamine", + "인공 향료 (Synthetic Fragrance, Parfum)": "syntheticFragrance", + "페녹시에탄올": "phenoxyethanol" } Return true only if the exact full Korean ingredient name appears continuously and separately; otherwise, return false. Ignore partial, similar, or incomplete matches. - - Note: "hicc" and "Hydroxyisohexyl 3-Cyclohexene Carboxaldehyde" are the same. """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); From 4e4830ba4139fc4f912a9689c9e561b7173ae077 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 16 May 2025 00:20:59 +0900 Subject: [PATCH 165/309] =?UTF-8?q?#36=20feat:=20=EC=8B=9D=ED=92=88=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=20=ED=95=84=EC=88=98=EA=B0=92=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/product/dto/request/FoodProductAnalysisRequest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java index 0de8c0b..fa37516 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java @@ -3,6 +3,7 @@ import com.webeye.backend.allergy.type.AllergyType; import com.webeye.backend.nutrition.domain.type.Gender; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Builder; @@ -15,6 +16,7 @@ public record FoodProductAnalysisRequest( @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") String productId, + @NotBlank(message = "상품 제목은 비어있을 수 없습니다.") @Schema(description = "상품 제목") String title, From 2ae38bcdd8c3339ac26c34b6462733427ffcb0c7 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 16 May 2025 00:28:33 +0900 Subject: [PATCH 166/309] =?UTF-8?q?#36=20refactor:=20=EC=9B=90=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9C=BC=EB=A9=B4=20warn=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rawmaterial/application/RawMaterialService.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java index 208c2d7..98313a1 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java @@ -11,6 +11,7 @@ import com.webeye.backend.rawmaterial.domain.RawMaterial; import com.webeye.backend.rawmaterial.persistent.RawMaterialRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,6 +19,7 @@ import java.util.Map; import java.util.Optional; +@Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -32,8 +34,7 @@ public class RawMaterialService { public void saveRawMaterialNutrition(Product product, FoodProductAnalysisRequest request) { String foodName = openAiClient.explainRawMaterial(request).name(); Optional rawMaterialOpt = rawMaterialRepository.findFirstByNameContaining(foodName); - - rawMaterialOpt.ifPresent(rawMaterial -> { + rawMaterialOpt.ifPresentOrElse(rawMaterial -> { Map nutrientMap = convertToNutrientMap(rawMaterial); nutrientMap.forEach((type, amount) -> { @@ -49,9 +50,9 @@ public void saveRawMaterialNutrition(Product product, FoodProductAnalysisRequest }); productRepository.save(product); - }); + }, () -> log.warn("일치하는 원재료 데이터가 존재하지 않습니다.")); } - + private Map convertToNutrientMap(RawMaterial rawMaterial) { Map map = new EnumMap<>(NutrientType.class); map.put(NutrientType.SODIUM, rawMaterial.getSodium()); From 67edf3a3a5c26f9d19aafe0370391a8502f4e381 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 16 May 2025 00:37:56 +0900 Subject: [PATCH 167/309] =?UTF-8?q?#36=20feat:=20ProductNutrient=20@Unique?= =?UTF-8?q?Constraint=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/product/domain/ProductNutrient.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java index dbe2788..9be4523 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java +++ b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java @@ -8,6 +8,9 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + uniqueConstraints = @UniqueConstraint(columnNames = {"product_id", "nutrient_id"}) +) public class ProductNutrient extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From 9bf4db252e223e27b72f0cb39f9516b2100a139e Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 16 May 2025 17:55:48 +0900 Subject: [PATCH 168/309] =?UTF-8?q?#44=20feat:=20=EC=83=81=ED=92=88=20Outl?= =?UTF-8?q?ineType=20enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/domain/type/OutlineType.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/com/webeye/backend/product/domain/type/OutlineType.java diff --git a/src/main/java/com/webeye/backend/product/domain/type/OutlineType.java b/src/main/java/com/webeye/backend/product/domain/type/OutlineType.java new file mode 100644 index 0000000..d8a3cb5 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/domain/type/OutlineType.java @@ -0,0 +1,19 @@ +package com.webeye.backend.product.domain.type; + +import lombok.Getter; + +@Getter +public enum OutlineType { + MAIN("내가 보낸 상품 설명 이미지를 분석하여 상품의 주된 성분, 소재, 기능, 구성 요소 등에 관한 정보를 알려줘. 특히, 식품이라면 영양성분은 필수로 알려줘. (ex. (영양)성분, 재질, 기능성, 구성품, 원산지, 제조사)"), + USAGE("내가 보낸 상품 설명 이미지를 분석하여 상품의 사용(섭취/장착/조립/활용) 방법 및 사용 대상을 알려줘. (ex. 섭취법, 사용 순서, 권장 대상, 연령대, 설치 방식)"), + WARNING("내가 보낸 상품 설명 이미지를 분석하여 상품의 보관법, 유통기한, 안전 주의, 알러지, 세척 등에 관한 정보를 알려줘. 특히, 식품이라면 알러지에 대한 정보는 필수로 알려줘. (ex. 보관법, 직사광선 피함, 유통기한, 세탁, 금지사항)"), + SPECS("내가 보낸 상품 설명 이미지를 분석하여 상품의 색상, 크기, 무게, 용량, 호환성, 구매 선택지 등에 대한 정보를 알려줘. (ex. 크기, 무게, 용량, 호수, 색상, 옵션 구성, 커버력)"), + CERTIFICATION("내가 보낸 상품 설명 이미지를 분석하여 상품의 상품의 인증, A/S, 포장, 브랜드, 배송, 마크 등에 대한 정보를 알려줘. (ex. KC인증, GMP, 비건, 식약처, 무상 A/S, 패키징)") + ; + + private final String prompt; + + OutlineType(String prompt) { + this.prompt = prompt; + } +} From a466e5e0d08899a03372b3bdb398b1bdd2adb24b Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 16 May 2025 18:48:46 +0900 Subject: [PATCH 169/309] =?UTF-8?q?#44=20remove:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B2=8C=20=EB=90=9C=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/AllergyController.java | 27 ------------ .../presentation/swagger/AllergySwagger.java | 28 ------------ .../application/ExplanationService.java | 23 ---------- .../response/PointExplanationResponse.java | 11 ----- .../presentation/ExplanationController.java | 37 ---------------- .../swagger/ExplanationSwagger.java | 44 ------------------- 6 files changed, 170 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java delete mode 100644 src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java delete mode 100644 src/main/java/com/webeye/backend/explanation/application/ExplanationService.java delete mode 100644 src/main/java/com/webeye/backend/explanation/dto/response/PointExplanationResponse.java delete mode 100644 src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java delete mode 100644 src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java diff --git a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java b/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java deleted file mode 100644 index 439b3ec..0000000 --- a/src/main/java/com/webeye/backend/allergy/presentation/AllergyController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.webeye.backend.allergy.presentation; - -import com.webeye.backend.allergy.application.AllergyService; -import com.webeye.backend.allergy.dto.response.AllergyAiResponse; -import com.webeye.backend.allergy.presentation.swagger.AllergySwagger; -import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -import static com.webeye.backend.global.dto.response.type.SuccessCode.ALLERGY_ANALYSIS_SUCCESS; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/v1/allergy") -public class AllergyController implements AllergySwagger { - private final AllergyService allergyService; - - @Override - @ResponseStatus(HttpStatus.OK) - @PostMapping(value = "") - public SuccessResponse allergyAnalysis(@Valid @RequestBody FoodProductAnalysisRequest request) { - return SuccessResponse.of(ALLERGY_ANALYSIS_SUCCESS ,allergyService.analyzeAllergy(request)); - } -} diff --git a/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java b/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java deleted file mode 100644 index 5b3ed49..0000000 --- a/src/main/java/com/webeye/backend/allergy/presentation/swagger/AllergySwagger.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.webeye.backend.allergy.presentation.swagger; - -import com.webeye.backend.allergy.dto.response.AllergyAiResponse; -import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.RequestBody; - -@Tag(name = "Allergy", description = "제품 알러지 유발 성분 관련 API") -public interface AllergySwagger { - @Operation( - summary = "제품의 알러지 유발 성분 추출", - description = "음식 제품에 대한 알러지 유발 성분을 추출합니다." - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "알러지 유발 요소가 성공적으로 추출되었습니다." - ) - }) - SuccessResponse allergyAnalysis( - @RequestBody FoodProductAnalysisRequest request - ); - -} diff --git a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java b/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java deleted file mode 100644 index 1bc6217..0000000 --- a/src/main/java/com/webeye/backend/explanation/application/ExplanationService.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.webeye.backend.explanation.application; - -import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; -import com.webeye.backend.explanation.dto.request.ProductDetailAnalysisRequest; -import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; -import com.webeye.backend.explanation.dto.response.PointExplanationResponse; -import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ExplanationService { - private final OpenAiClient openAiClient; - - public PointExplanationResponse analyzeProductPoint(ProductAnalysisRequest request) { - return openAiClient.explainProductPoint(request); - } - - public DetailExplanationResponse analyzeProductDetail(ProductDetailAnalysisRequest request) { - return openAiClient.explainProductDetail(request); - } -} diff --git a/src/main/java/com/webeye/backend/explanation/dto/response/PointExplanationResponse.java b/src/main/java/com/webeye/backend/explanation/dto/response/PointExplanationResponse.java deleted file mode 100644 index d8324dd..0000000 --- a/src/main/java/com/webeye/backend/explanation/dto/response/PointExplanationResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.webeye.backend.explanation.dto.response; - -import lombok.Builder; - -import java.util.List; - -@Builder -public record PointExplanationResponse( - List keyFeatures -) { -} diff --git a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java b/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java deleted file mode 100644 index c77d860..0000000 --- a/src/main/java/com/webeye/backend/explanation/presentation/ExplanationController.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.webeye.backend.explanation.presentation; - -import com.webeye.backend.explanation.application.ExplanationService; -import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; -import com.webeye.backend.explanation.dto.request.ProductDetailAnalysisRequest; -import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; -import com.webeye.backend.explanation.dto.response.PointExplanationResponse; -import com.webeye.backend.explanation.presentation.swagger.ExplanationSwagger; -import com.webeye.backend.global.dto.response.SuccessResponse; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -import static com.webeye.backend.global.dto.response.type.SuccessCode.PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS; -import static com.webeye.backend.global.dto.response.type.SuccessCode.PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS; - -@RequiredArgsConstructor -@RestController -@RequestMapping("/v1/explanation") -public class ExplanationController implements ExplanationSwagger { - private final ExplanationService explanationService; - - @Override - @ResponseStatus(HttpStatus.OK) - @PostMapping(value = "") - public SuccessResponse productAnalysis(@Valid @RequestBody ProductAnalysisRequest request) { - return SuccessResponse.of(PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS, explanationService.analyzeProductPoint(request)); - } - - @Override - @ResponseStatus(HttpStatus.OK) - @PostMapping(value = "/detail") - public SuccessResponse productDetailAnalysis(@Valid @RequestBody ProductDetailAnalysisRequest request) { - return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, explanationService.analyzeProductDetail(request)); - } -} diff --git a/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java b/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java deleted file mode 100644 index 0d7815b..0000000 --- a/src/main/java/com/webeye/backend/explanation/presentation/swagger/ExplanationSwagger.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.webeye.backend.explanation.presentation.swagger; - -import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; -import com.webeye.backend.explanation.dto.request.ProductDetailAnalysisRequest; -import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; -import com.webeye.backend.explanation.dto.response.PointExplanationResponse; -import com.webeye.backend.global.dto.response.SuccessResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.RequestBody; - -@Tag(name = "Explanation", description = "제품 설명 관련 API") -public interface ExplanationSwagger { - @Operation( - summary = "제품 설명 이미지에서 주요 요소 추출", - description = "제품 설명 이미지를 입력받아 주요 설명 요소를 추출합니다." - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "제품 주요 설명 요소가 추출되었습니다." - ) - }) - SuccessResponse productAnalysis( - @RequestBody ProductAnalysisRequest request - ); - - @Operation( - summary = "제품 주요 요소에 대한 상세 설명 추출", - description = "제품 설명 이미지를 입력받아 주요 요소에 대한 상세 설명을 추출합니다." - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "제품 주요 요소에 대한 상세 설명이 추출되었습니다." - ) - }) - SuccessResponse productDetailAnalysis( - @RequestBody ProductDetailAnalysisRequest request - ); -} - From f68f2dd5a5ec4ec4ef889eba5f27e71155e62429 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 16 May 2025 18:49:50 +0900 Subject: [PATCH 170/309] =?UTF-8?q?refactor:=20ProductAnalysis=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20DTO=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cosmetic/application/CosmeticService.java | 2 +- .../cosmetic/application/CosmeticServiceImpl.java | 2 +- .../cosmetic/presentation/CosmeticController.java | 2 +- .../presentation/swagger/CosmeticSwagger.java | 2 +- .../dto/request/ProductAnalysisRequest.java | 4 ++-- .../dto/request/ProductDetailAnalysisRequest.java | 12 +++--------- .../dto/response/DetailExplanationResponse.java | 2 +- 7 files changed, 10 insertions(+), 16 deletions(-) rename src/main/java/com/webeye/backend/{explanation => product}/dto/request/ProductAnalysisRequest.java (90%) rename src/main/java/com/webeye/backend/{explanation => product}/dto/request/ProductDetailAnalysisRequest.java (53%) rename src/main/java/com/webeye/backend/{explanation => product}/dto/response/DetailExplanationResponse.java (66%) diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java index 456359e..65f452b 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java @@ -1,7 +1,7 @@ package com.webeye.backend.cosmetic.application; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; -import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; public interface CosmeticService { CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request); diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java index ff34cfe..2f6559d 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java @@ -1,8 +1,8 @@ package com.webeye.backend.cosmetic.application; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; -import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java b/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java index c7785a2..4772e29 100644 --- a/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java +++ b/src/main/java/com/webeye/backend/cosmetic/presentation/CosmeticController.java @@ -3,8 +3,8 @@ import com.webeye.backend.cosmetic.application.CosmeticService; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.cosmetic.presentation.swagger.CosmeticSwagger; -import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.parameters.RequestBody; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java b/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java index ea8881e..d6f3dcf 100644 --- a/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java +++ b/src/main/java/com/webeye/backend/cosmetic/presentation/swagger/CosmeticSwagger.java @@ -1,8 +1,8 @@ package com.webeye.backend.cosmetic.presentation.swagger; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; -import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; diff --git a/src/main/java/com/webeye/backend/explanation/dto/request/ProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java similarity index 90% rename from src/main/java/com/webeye/backend/explanation/dto/request/ProductAnalysisRequest.java rename to src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java index 8475bc3..77ae093 100644 --- a/src/main/java/com/webeye/backend/explanation/dto/request/ProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java @@ -1,4 +1,4 @@ -package com.webeye.backend.explanation.dto.request; +package com.webeye.backend.product.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; @@ -16,4 +16,4 @@ public record ProductAnalysisRequest( @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") List urls ) { -} +} \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/explanation/dto/request/ProductDetailAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java similarity index 53% rename from src/main/java/com/webeye/backend/explanation/dto/request/ProductDetailAnalysisRequest.java rename to src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java index 2dd6726..e5bbfe9 100644 --- a/src/main/java/com/webeye/backend/explanation/dto/request/ProductDetailAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java @@ -1,4 +1,4 @@ -package com.webeye.backend.explanation.dto.request; +package com.webeye.backend.product.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; @@ -6,17 +6,11 @@ import java.util.List; -@Schema(description = "제품 상세 분석") +@Schema(description = "상품 설명 이미지의 URL") @Builder public record ProductDetailAnalysisRequest( - @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") - String productId, - - @NotEmpty(message = "제품 설명 요소는 비어있을 수 없습니다.") - String description, - @Schema(description = "상품 이미지 URL") @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") List urls -) { +){ } diff --git a/src/main/java/com/webeye/backend/explanation/dto/response/DetailExplanationResponse.java b/src/main/java/com/webeye/backend/product/dto/response/DetailExplanationResponse.java similarity index 66% rename from src/main/java/com/webeye/backend/explanation/dto/response/DetailExplanationResponse.java rename to src/main/java/com/webeye/backend/product/dto/response/DetailExplanationResponse.java index 4e4471c..42736ba 100644 --- a/src/main/java/com/webeye/backend/explanation/dto/response/DetailExplanationResponse.java +++ b/src/main/java/com/webeye/backend/product/dto/response/DetailExplanationResponse.java @@ -1,4 +1,4 @@ -package com.webeye.backend.explanation.dto.response; +package com.webeye.backend.product.dto.response; import lombok.Builder; From 8a17d574b4aaef4a85135026de6ddd8876a50c4e Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 16 May 2025 18:50:23 +0900 Subject: [PATCH 171/309] =?UTF-8?q?#44=20feat:=20=EA=B0=9C=EC=9A=94?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=83=81=ED=92=88=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B6=84=EC=84=9D=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ProductController.java | 12 ++++++++++ .../presentation/swagger/ProductSwagger.java | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/presentation/ProductController.java b/src/main/java/com/webeye/backend/product/presentation/ProductController.java index 044da19..7f01175 100644 --- a/src/main/java/com/webeye/backend/product/presentation/ProductController.java +++ b/src/main/java/com/webeye/backend/product/presentation/ProductController.java @@ -1,8 +1,11 @@ package com.webeye.backend.product.presentation; +import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.product.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.product.application.ProductService; +import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.product.dto.response.ProductResponse; import com.webeye.backend.product.presentation.swagger.ProductSwagger; import jakarta.validation.Valid; @@ -11,6 +14,7 @@ import org.springframework.web.bind.annotation.*; import static com.webeye.backend.global.dto.response.type.SuccessCode.FOOD_PRODUCT_ANALYSIS_SUCCESS; +import static com.webeye.backend.global.dto.response.type.SuccessCode.PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS; @RequiredArgsConstructor @RestController @@ -25,4 +29,12 @@ public SuccessResponse foodAnalysis(@Valid @RequestBody FoodPro return SuccessResponse.of(FOOD_PRODUCT_ANALYSIS_SUCCESS, productService.analyzeFoodProduct(request)); } + @Override + @ResponseStatus(HttpStatus.OK) + @PostMapping(value = "/analysis/{outline}") + public SuccessResponse productDetailAnalysis( + @PathVariable OutlineType outline, @Valid @RequestBody ProductDetailAnalysisRequest request) { + return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, productService.analyzeProductDetail(outline, request)); + } + } diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java index 3b04f36..787a556 100644 --- a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -1,9 +1,14 @@ package com.webeye.backend.product.presentation.swagger; +import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.product.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; +import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.product.dto.response.ProductResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -24,4 +29,21 @@ public interface ProductSwagger { SuccessResponse foodAnalysis( @RequestBody FoodProductAnalysisRequest request ); + + @Operation( + summary = "제품 주요 개요에 대한 상세 설명 추출", + description = "제품 설명 이미지와 개요를 입력받아 주요 요소에 대한 상세 설명을 추출합니다. " + + "MAIN: 주요정보, USAGE: 사용정보, WARNING: 주의 및 보관, SPECS: 규격 및 옵션, CERTIFICATION: 인증 및 기타" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "제품 주요 요소에 대한 상세 설명이 추출되었습니다." + ) + }) + SuccessResponse productDetailAnalysis( + @Parameter(in = ParameterIn.PATH, description = "상품 개요", required = true) + OutlineType outline, + @RequestBody ProductDetailAnalysisRequest request + ); } From 88c53df8d373fb3f7a97da90dcaff2cb97ae80fc Mon Sep 17 00:00:00 2001 From: yeonjy Date: Fri, 16 May 2025 18:51:00 +0900 Subject: [PATCH 172/309] =?UTF-8?q?#44=20feat:=20=EC=83=81=ED=92=88=20outl?= =?UTF-8?q?ine=EC=9D=84=20=ED=99=9C=EC=9A=A9=ED=95=9C=20=EB=B6=84=EC=84=9D?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/OpenAiClient.java | 43 ++++--------------- .../product/application/ProductService.java | 10 ++++- .../product/domain/type/OutlineType.java | 10 ++--- 3 files changed, 23 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index eca7942..4301643 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -2,15 +2,15 @@ import com.webeye.backend.allergy.dto.response.AllergyAiResponse; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; -import com.webeye.backend.explanation.dto.request.ProductAnalysisRequest; -import com.webeye.backend.explanation.dto.request.ProductDetailAnalysisRequest; -import com.webeye.backend.explanation.dto.response.DetailExplanationResponse; -import com.webeye.backend.explanation.dto.response.PointExplanationResponse; +import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; +import com.webeye.backend.product.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; +import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.rawmaterial.dto.response.RawMaterialAiResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,43 +36,18 @@ public class OpenAiClient { private final ChatClient chatClient; - public PointExplanationResponse explainProductPoint(ProductAnalysisRequest request) { - String system = """ - You are an expert in analyzing product images and extracting key information that helps users make purchase decisions. - Based on the image provided, extract only the most relevant and concise information that highlights the product's core value. - The extracted list should contain no more than 6 items. Do not include unnecessary or overly detailed information—only what's essential for a buyer. - """; - String user = """ - Analyze the image and extract key product information in the format below. - You may adjust the content depending on the product type, but make sure the extracted items are helpful for making a purchase decision. - Limit the result to a maximum of 6 items, and keep each one clear and concise. - Below is just a guideline — instead of following the content exactly, follow the intent. - - Example (Generate appropriately according to the product) - - 핵심 특징 요약 (제품 포인트) - - 당류 비교 (10g당 당류 함량 비교) - - 제품 추천 대상 - - 상세 원재료 - - 영양 정보 (1개 10g 기준) - """; - - ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request.urls(), prompt, PointExplanationResponse.class); - } - - public DetailExplanationResponse explainProductDetail(ProductDetailAnalysisRequest request) { + public DetailExplanationResponse explainProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { String system = """ You are an expert in providing detailed explanations about products based on images. - When a user provides a product description image along with the key elements of that description, you should offer a clear and detailed explanation of that element. + When a user provides a product description image along with the key outline of that description, you should offer a clear and detailed explanation of that element. In this explanation, you must provide very detailed information about that element from the image. Answer in Korean. """; String user = String.format(""" - Key descriptive element: %s - I have provided a product description image along with the key descriptive elements extracted from the image. - Please generate a detailed explanation of the provided key descriptive element. - """, request.description()); + Key Outline: %s + Please generate a detailed explanation of the provided outline. + """, outline.getPrompt()); ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 9e0794f..612d09a 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -2,15 +2,19 @@ import com.webeye.backend.allergy.application.AllergyService; import com.webeye.backend.allergy.type.AllergyType; +import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.nutrition.application.NutrientRecommendationService; import com.webeye.backend.nutrition.dto.request.NutrientRecommendationRequest; import com.webeye.backend.nutrition.dto.response.NutrientRecommendationResponse; import com.webeye.backend.product.domain.ProductAllergy; +import com.webeye.backend.product.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.application.NutritionService; import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.product.dto.response.ProductResponse; import com.webeye.backend.product.persistent.ProductRepository; import lombok.RequiredArgsConstructor; @@ -26,6 +30,7 @@ public class ProductService { private final NutritionService nutritionService; private final NutrientRecommendationService nutrientRecommendationService; private final AllergyService allergyService; + private final OpenAiClient openAiClient; private final ProductRepository productRepository; @@ -40,7 +45,6 @@ public ProductResponse analyzeFoodProduct(FoodProductAnalysisRequest request) { .overRecommendationNutrients(getNutrientRecommendationResponse(request, product)) .build(); } - Product product = Product.builder() .id(request.productId()) .build(); @@ -68,4 +72,8 @@ private List getNutrientRecommendationResponse(F .builder().birthYear(request.birthYear()).gender(request.gender()).product(product).build()); } + public DetailExplanationResponse analyzeProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { + return openAiClient.explainProductDetail(outline, request); + } + } diff --git a/src/main/java/com/webeye/backend/product/domain/type/OutlineType.java b/src/main/java/com/webeye/backend/product/domain/type/OutlineType.java index d8a3cb5..53e9700 100644 --- a/src/main/java/com/webeye/backend/product/domain/type/OutlineType.java +++ b/src/main/java/com/webeye/backend/product/domain/type/OutlineType.java @@ -4,11 +4,11 @@ @Getter public enum OutlineType { - MAIN("내가 보낸 상품 설명 이미지를 분석하여 상품의 주된 성분, 소재, 기능, 구성 요소 등에 관한 정보를 알려줘. 특히, 식품이라면 영양성분은 필수로 알려줘. (ex. (영양)성분, 재질, 기능성, 구성품, 원산지, 제조사)"), - USAGE("내가 보낸 상품 설명 이미지를 분석하여 상품의 사용(섭취/장착/조립/활용) 방법 및 사용 대상을 알려줘. (ex. 섭취법, 사용 순서, 권장 대상, 연령대, 설치 방식)"), - WARNING("내가 보낸 상품 설명 이미지를 분석하여 상품의 보관법, 유통기한, 안전 주의, 알러지, 세척 등에 관한 정보를 알려줘. 특히, 식품이라면 알러지에 대한 정보는 필수로 알려줘. (ex. 보관법, 직사광선 피함, 유통기한, 세탁, 금지사항)"), - SPECS("내가 보낸 상품 설명 이미지를 분석하여 상품의 색상, 크기, 무게, 용량, 호환성, 구매 선택지 등에 대한 정보를 알려줘. (ex. 크기, 무게, 용량, 호수, 색상, 옵션 구성, 커버력)"), - CERTIFICATION("내가 보낸 상품 설명 이미지를 분석하여 상품의 상품의 인증, A/S, 포장, 브랜드, 배송, 마크 등에 대한 정보를 알려줘. (ex. KC인증, GMP, 비건, 식약처, 무상 A/S, 패키징)") + MAIN("Analyze the product description image I send and provide information about the main ingredients, materials, functions, and components of the product. If it is a food item, nutritional information is required. Respond in Korean. (e.g., ingredients/nutritional content, material, functionality, components, country of origin, manufacturer)"), + USAGE("Analyze the product description image I send and provide information about how to use (consume/assemble/install/utilize) the product and the intended user. Respond in Korean. (e.g., consumption method, usage steps, recommended users, age group, installation method)"), + WARNING("Analyze the product description image I send and provide information about storage methods, expiration date, safety precautions, allergies, cleaning, etc. If it is a food item, allergy information is required. Respond in Korean. (e.g., storage method, avoid direct sunlight, expiration date, washing, prohibitions)"), + SPECS("Analyze the product description image I send and provide information about the product’s color, size, weight, capacity, compatibility, and purchasing options. Respond in Korean. (e.g., size, weight, capacity, size options, color, option configuration, coverage)"), + CERTIFICATION("Analyze the product description image I send and provide information about the product’s certifications, after-sales service, packaging, brand, delivery, and marks. Respond in Korean. (e.g., KC certification, GMP, vegan, KFDA, free A/S, packaging)") ; private final String prompt; From c27e2011fb1be25af58dad6af076f7b16783fbf3 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 00:04:23 +0900 Subject: [PATCH 173/309] #47 feat: Image Analysis API --- .../global/dto/response/type/SuccessCode.java | 15 +++-------- .../presentation/ImageAnalysisController.java | 27 +++++++++++++++++++ .../swagger/ImageAnalysisSwagger.java | 27 +++++++++++++++++++ 3 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/webeye/backend/imageanalysis/presentation/ImageAnalysisController.java create mode 100644 src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index c6f3cb7..cf2aa00 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -12,23 +12,14 @@ public enum SuccessCode { // product FOOD_PRODUCT_ANALYSIS_SUCCESS(200, "Product Analysis Success"), + PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS(200, "Product detail explanation analysis success"), - // raw material - RAW_MATERIAL_API_SUCCESS(200, "Raw Material API Success"), - - // nutrition - NUTRITION_ANALYSIS_SUCCESS(200, "Nutrition analysis success"), - - // allergy - ALLERGY_ANALYSIS_SUCCESS(200, "Allergy analysis success"), + // explanation + IMAGE_ANALYSIS_SUCCESS(200, "ImageAnalysis Success"), // cosmetic COSMETIC_ANALYSIS_SUCCESS(200, "Cosmetic analysis success"), - // explanation - PRODUCT_POINT_EXPLANATION_ANALYSIS_SUCCESS(200, "Product point explanation analysis success"), - PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS(200, "Product detail explanation analysis success"), - // review REVIEW_SUMMARY_SUCCESS(200, "Review summary success"), diff --git a/src/main/java/com/webeye/backend/imageanalysis/presentation/ImageAnalysisController.java b/src/main/java/com/webeye/backend/imageanalysis/presentation/ImageAnalysisController.java new file mode 100644 index 0000000..0ae7075 --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/presentation/ImageAnalysisController.java @@ -0,0 +1,27 @@ +package com.webeye.backend.imageanalysis.presentation; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.imageanalysis.application.ImageAnalysisService; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.imageanalysis.dto.response.ImageAnalysisResponse; +import com.webeye.backend.imageanalysis.presentation.swagger.ImageAnalysisSwagger; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.IMAGE_ANALYSIS_SUCCESS; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/image-analysis") +public class ImageAnalysisController implements ImageAnalysisSwagger { + private final ImageAnalysisService imageAnalysisService; + + @Override + @ResponseStatus(HttpStatus.OK) + @PostMapping(value = "") + public SuccessResponse imageAnalysis(@Valid @RequestBody ImageAnalysisRequest request) { + return SuccessResponse.of(IMAGE_ANALYSIS_SUCCESS, imageAnalysisService.analyzeImage(request)); + } +} diff --git a/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java b/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java new file mode 100644 index 0000000..dceb8cd --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java @@ -0,0 +1,27 @@ +package com.webeye.backend.imageanalysis.presentation.swagger; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.imageanalysis.dto.response.ImageAnalysisResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "이미지 분석", description = "이미지 분석 관련 API") +public interface ImageAnalysisSwagger { + @Operation( + summary = "[이미지 분석]", + description = "이미지 URL을 입력받아 이미지를 분석을 제공합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "이미지가 성공적으로 분석되었습니다." + ) + }) + SuccessResponse imageAnalysis( + @RequestBody ImageAnalysisRequest request + ); +} From 6a0bddc02586924cdf929e6c674fc4652fb669ef Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 00:04:49 +0900 Subject: [PATCH 174/309] #47 feat: ImageAnalysis Service --- .../application/ImageAnalysisService.java | 18 ++++++++++++++++++ .../dto/request/ImageAnalysisRequest.java | 14 ++++++++++++++ .../dto/response/ImageAnalysisResponse.java | 11 +++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/main/java/com/webeye/backend/imageanalysis/application/ImageAnalysisService.java create mode 100644 src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java create mode 100644 src/main/java/com/webeye/backend/imageanalysis/dto/response/ImageAnalysisResponse.java diff --git a/src/main/java/com/webeye/backend/imageanalysis/application/ImageAnalysisService.java b/src/main/java/com/webeye/backend/imageanalysis/application/ImageAnalysisService.java new file mode 100644 index 0000000..ce1ed4a --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/application/ImageAnalysisService.java @@ -0,0 +1,18 @@ +package com.webeye.backend.imageanalysis.application; + +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.imageanalysis.dto.response.ImageAnalysisResponse; +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ImageAnalysisService { + private final OpenAiClient openAiClient; + + public ImageAnalysisResponse analyzeImage(ImageAnalysisRequest request) { + return openAiClient.explainImage(request); + } + +} diff --git a/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java b/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java new file mode 100644 index 0000000..f625f08 --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/dto/request/ImageAnalysisRequest.java @@ -0,0 +1,14 @@ +package com.webeye.backend.imageanalysis.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import org.hibernate.validator.constraints.URL; + +@Schema(description = "이미지 분석") +public record ImageAnalysisRequest( + @Schema(description = "상품 이미지 URL") + @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") + @URL + String url +) { +} diff --git a/src/main/java/com/webeye/backend/imageanalysis/dto/response/ImageAnalysisResponse.java b/src/main/java/com/webeye/backend/imageanalysis/dto/response/ImageAnalysisResponse.java new file mode 100644 index 0000000..d86237a --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/dto/response/ImageAnalysisResponse.java @@ -0,0 +1,11 @@ +package com.webeye.backend.imageanalysis.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema(description = "이미지 분석 응답") +@Builder +public record ImageAnalysisResponse( + String analysis +) { +} From 26914ea783cdc514b57cecdb57da14acc8bca642 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 00:05:11 +0900 Subject: [PATCH 175/309] #47 feat: Image Analysis Open AI prompt --- .../infrastructure/OpenAiClient.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 4301643..3dcc753 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -2,6 +2,8 @@ import com.webeye.backend.allergy.dto.response.AllergyAiResponse; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; +import com.webeye.backend.imageanalysis.dto.response.ImageAnalysisResponse; import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.error.BusinessException; @@ -177,6 +179,21 @@ Ignore quantities (mg, %, g), descriptors (함유, 분말), or anything in paren return callWithStructuredOutput(request.urls(), prompt, HealthFoodAiResponse.class); } + public ImageAnalysisResponse explainImage(ImageAnalysisRequest request) { + String system = """ + You are a helpful and concise assistant that specializes in describing images. + Always provide clear, accurate, and human-like descriptions of the image content. + Focus on the most important visual details such as objects, people, actions, scenes, and context. + Avoid speculation unless necessary, and do not include irrelevant information. Answer in Korean + """; + String user = """ + Please describe the contents of this image in detail. + """; + + ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); + return callWithStructuredOutput(List.of(request.url()), prompt, ImageAnalysisResponse.class); + } + private T callWithStructuredOutput(List urls, ImageAnalysisPrompt prompt, Class clazz) { BeanOutputConverter outputConverter = new BeanOutputConverter<>(clazz); From 3f4308ada30a25220d674e0947c597766339fe8b Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 00:07:03 +0900 Subject: [PATCH 176/309] =?UTF-8?q?#47=20docs:=20swagger=20API=20Tag=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=EC=9D=BC=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/swagger/ImageAnalysisSwagger.java | 2 +- .../backend/product/presentation/swagger/ProductSwagger.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java b/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java index dceb8cd..c712a4b 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java +++ b/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java @@ -9,7 +9,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.RequestBody; -@Tag(name = "이미지 분석", description = "이미지 분석 관련 API") +@Tag(name = "[이미지 분석]", description = "이미지 분석 관련 API") public interface ImageAnalysisSwagger { @Operation( summary = "[이미지 분석]", diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java index 787a556..b8fdc4c 100644 --- a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -14,7 +14,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.RequestBody; -@Tag(name = "Product", description = "제품 관련 API") +@Tag(name = "[제품]", description = "제품 관련 API") public interface ProductSwagger { @Operation( summary = "음식 제품 분석", From b5ebd57d9503fc800e38bb00df27fbb7bf38a0ab Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 18:15:10 +0900 Subject: [PATCH 177/309] =?UTF-8?q?#49=20feat:=20product=20detail=20html?= =?UTF-8?q?=EC=9D=84=20=EC=9E=85=EB=A0=A5=EB=B0=9B=EC=95=84=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20URL=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/OpenAiClient.java | 2 +- .../product/application/ProductService.java | 24 ++++++++++++++++++- .../request/ProductDetailAnalysisRequest.java | 8 +++---- .../presentation/swagger/ProductSwagger.java | 2 +- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 3dcc753..c7cfedd 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -39,7 +39,7 @@ public class OpenAiClient { private final ChatClient chatClient; - public DetailExplanationResponse explainProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { + public DetailExplanationResponse explainProductDetail(OutlineType outline, List urls) { String system = """ You are an expert in providing detailed explanations about products based on images. When a user provides a product description image along with the key outline of that description, you should offer a clear and detailed explanation of that element. diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 612d09a..fe10a6c 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -18,11 +18,20 @@ import com.webeye.backend.product.dto.response.ProductResponse; import com.webeye.backend.product.persistent.ProductRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +@Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -73,7 +82,20 @@ private List getNutrientRecommendationResponse(F } public DetailExplanationResponse analyzeProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { - return openAiClient.explainProductDetail(outline, request); + return openAiClient.explainProductDetail(outline, extractImageUrlFromHtml(request.html())); } + private List extractImageUrlFromHtml(String html) { + Pattern pattern = Pattern.compile("]+src=[\"'](//[^\"']+)[\"']"); + Matcher matcher = pattern.matcher(html); + + List imageUrls = new ArrayList<>(); + + while (matcher.find()) { + String rawUrl = matcher.group(1); + String fullUrl = "https:" + rawUrl; + imageUrls.add(fullUrl); + } + return imageUrls; + } } diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java index e5bbfe9..b71a7bf 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java @@ -4,13 +4,11 @@ import jakarta.validation.constraints.NotEmpty; import lombok.Builder; -import java.util.List; - -@Schema(description = "상품 설명 이미지의 URL") +@Schema(description = "상품 설명 이미지의 HTML") @Builder public record ProductDetailAnalysisRequest( - @Schema(description = "상품 이미지 URL") + @Schema(description = "상품 상세 이미지") @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") - List urls + String html ){ } diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java index b8fdc4c..7bd7885 100644 --- a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -32,7 +32,7 @@ SuccessResponse foodAnalysis( @Operation( summary = "제품 주요 개요에 대한 상세 설명 추출", - description = "제품 설명 이미지와 개요를 입력받아 주요 요소에 대한 상세 설명을 추출합니다. " + + description = "제품 상세 설명 이미지가 포함된 HTML과 개요를 입력받아 주요 요소에 대한 상세 설명을 추출합니다. " + "MAIN: 주요정보, USAGE: 사용정보, WARNING: 주의 및 보관, SPECS: 규격 및 옵션, CERTIFICATION: 인증 및 기타" ) @ApiResponses(value = { From f52a31f571be506c214979b32491b1dcc663f2a9 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 18:17:06 +0900 Subject: [PATCH 178/309] =?UTF-8?q?#49=20fix:=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=9C=20import=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 2 +- .../webeye/backend/product/application/ProductService.java | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index c7cfedd..2d652cb 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -53,7 +53,7 @@ public DetailExplanationResponse explainProductDetail(OutlineType outline, List< ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request.urls(), prompt, DetailExplanationResponse.class); + return callWithStructuredOutput(urls, prompt, DetailExplanationResponse.class); } public AllergyAiResponse explainAllergy(FoodProductAnalysisRequest request) { diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index fe10a6c..6ccbfe2 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -19,10 +19,6 @@ import com.webeye.backend.product.persistent.ProductRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; From 014d0316ae7e6be89dbd9b55362f0a496c3fece1 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 18:32:20 +0900 Subject: [PATCH 179/309] =?UTF-8?q?#49=20docs:=20swagger=EC=99=80=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/dto/request/ProductDetailAnalysisRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java index b71a7bf..9646142 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java @@ -7,8 +7,8 @@ @Schema(description = "상품 설명 이미지의 HTML") @Builder public record ProductDetailAnalysisRequest( - @Schema(description = "상품 상세 이미지") - @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") + @Schema(description = "상품 상세 정보 HTML") + @NotEmpty(message = "상품 상세 정보의 HTML은 비어있을 수 없습니다.") String html ){ } From ac63ade6e608f3f57ddad605e5abe955971222b5 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 18:38:49 +0900 Subject: [PATCH 180/309] =?UTF-8?q?#49=20chore:=20HTML=20->=20image=20urls?= =?UTF-8?q?=20=EC=B6=94=EC=B6=9C=20Log=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/product/application/ProductService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 6ccbfe2..01ece55 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -92,6 +92,9 @@ private List extractImageUrlFromHtml(String html) { String fullUrl = "https:" + rawUrl; imageUrls.add(fullUrl); } + log.info("extracted urls: {}", imageUrls); + log.info("total number of images: {}", imageUrls.size()); + return imageUrls; } } From 29e7be29fb7a857a062cfe227ce0be28c3cd5ff0 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 17 May 2025 18:50:36 +0900 Subject: [PATCH 181/309] =?UTF-8?q?#50=20refactor:=20request=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20String=20->=20Map=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 6 ++++-- .../dto/request/ReviewSummaryRequest.java | 5 ++--- .../clovaX/ClovaXClientService.java | 21 +++++++++++++++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 0c866b3..dfdcc30 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -10,6 +10,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Map; + @Service @RequiredArgsConstructor public class ReviewService { @@ -19,9 +21,9 @@ public class ReviewService { @Transactional public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { - String reviewText = String.join("\n", request.reviews()); + Map> reviewMap = request.reviews(); - ReviewSummaryResponse response = clovaXClientService.summarizeReviewText(reviewText); + ReviewSummaryResponse response = clovaXClientService.summarizeReviewText(reviewMap); Review review = ReviewMapper.toEntity(response); reviewRepository.save(review); diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 164bdf3..24d5e0c 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -3,12 +3,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; -import java.util.List; +import java.util.Map; @Builder @Schema(description = "리뷰 목록") public record ReviewSummaryRequest( - @Schema(description = "쿠팡 리뷰 목록", example = "[맛있어요, 배송 느려요, 부드러워요]") - List reviews + Map> reviews ) { } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 0f803be..fc4211c 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service @@ -28,7 +29,9 @@ public class ClovaXClientService { @Value("${clova.request-id}") private String requestId; - public ReviewSummaryResponse summarizeReviewText(String reviewText) { + public ReviewSummaryResponse summarizeReviewText(Map> reviewText) { + String inputText = convertReviewMapToText(reviewText); + ClovaXRequest clovaXRequest = new ClovaXRequest(List.of( new ClovaXMessage(Role.SYSTEM, List.of( new ClovaXContent(ContentType.TEXT, @@ -49,7 +52,7 @@ public ReviewSummaryResponse summarizeReviewText(String reviewText) { ) )), new ClovaXMessage(Role.USER, List.of( - new ClovaXContent(ContentType.TEXT, reviewText) + new ClovaXContent(ContentType.TEXT, inputText) )) )); ClovaXResponse clovaXResponse = clovaXClient.createReviewSummary("Bearer "+ secretKey, requestId, clovaXRequest); @@ -77,4 +80,18 @@ private ReviewSummaryResponse parseResponse(String content) { return new ReviewSummaryResponse(positive, negative, keywords); } + + private String convertReviewMapToText(Map> reviewMap) { + StringBuilder sb = new StringBuilder(); + + for (Map.Entry> entry : reviewMap.entrySet()) { + sb.append("[").append(entry.getKey()).append("]\n"); + for (Map.Entry option : entry.getValue().entrySet()) { + sb.append("- ").append(option.getKey()).append(": ").append(option.getValue()).append("%\n"); + } + sb.append("\n"); + } + return sb.toString(); + } + } From b7843f06323b522a13c8620c07d0aa96828162b8 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 17 May 2025 18:52:50 +0900 Subject: [PATCH 182/309] =?UTF-8?q?#50=20refactor:=20=ED=94=84=EB=A1=AC?= =?UTF-8?q?=ED=94=84=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/infrastructure/clovaX/ClovaXClientService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index fc4211c..7b35913 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -40,7 +40,7 @@ public ReviewSummaryResponse summarizeReviewText(Map Date: Sat, 17 May 2025 18:58:28 +0900 Subject: [PATCH 183/309] =?UTF-8?q?#49=20refactor:=20image=20url=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20ImageUrlExtr?= =?UTF-8?q?actor=20=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/ImageUrlExtractor.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java new file mode 100644 index 0000000..cf29974 --- /dev/null +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -0,0 +1,30 @@ +package com.webeye.backend.imageanalysis.infrastructure; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Slf4j +@Component +public class ImageUrlExtractor { + public List extractImageUrlFromHtml(String html) { + Pattern pattern = Pattern.compile("]+src=[\"'](//[^\"']+)[\"']"); + Matcher matcher = pattern.matcher(html); + + List imageUrls = new ArrayList<>(); + + while (matcher.find()) { + String rawUrl = matcher.group(1); + String fullUrl = "https:" + rawUrl; + imageUrls.add(fullUrl); + } + log.info("extracted urls: {}", imageUrls); + log.info("total number of images: {}", imageUrls.size()); + + return imageUrls; + } +} From 6f5c54b41c6603765e40dfe6c8a95788666c7491 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 19:00:06 +0900 Subject: [PATCH 184/309] =?UTF-8?q?#49=20refactor:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20List=EB=A1=9C=20=EB=B0=9B=EB=8A=94=20API?= =?UTF-8?q?=EB=A5=BC=20HTML=EB=A1=9C=20=EB=B0=9B=EC=95=84=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../allergy/application/AllergyService.java | 4 ++- .../application/CosmeticServiceImpl.java | 4 ++- .../application/HealthFoodService.java | 4 ++- .../infrastructure/OpenAiClient.java | 17 ++++++------ .../application/NutritionService.java | 4 ++- .../product/application/ProductService.java | 27 ++++--------------- .../request/FoodProductAnalysisRequest.java | 6 ++--- .../dto/request/ProductAnalysisRequest.java | 8 +++--- 8 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java index 021027b..95646c1 100644 --- a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java +++ b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java @@ -1,6 +1,7 @@ package com.webeye.backend.allergy.application; import com.webeye.backend.allergy.dto.response.AllergyAiResponse; +import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.product.domain.Product; @@ -14,10 +15,11 @@ @RequiredArgsConstructor public class AllergyService { private final OpenAiClient openAiClient; + private final ImageUrlExtractor imageUrlExtractor; private final ProductRepository productRepository; public AllergyAiResponse analyzeAllergy(FoodProductAnalysisRequest request) { - return openAiClient.explainAllergy(request); + return openAiClient.explainAllergy(imageUrlExtractor.extractImageUrlFromHtml(request.html())); } @Transactional diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java index 2f6559d..46e0c65 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java @@ -1,6 +1,7 @@ package com.webeye.backend.cosmetic.application; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import lombok.RequiredArgsConstructor; @@ -11,9 +12,10 @@ public class CosmeticServiceImpl implements CosmeticService { private final OpenAiClient openAiClient; + private final ImageUrlExtractor imageUrlExtractor; @Override public CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request) { - return openAiClient.explainCosmetic(request); + return openAiClient.explainCosmetic(imageUrlExtractor.extractImageUrlFromHtml(request.html())); } } diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 27298f6..e9db47e 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -11,6 +11,7 @@ import com.webeye.backend.healthfood.infrastructure.mapper.HealthFoodMapper; import com.webeye.backend.healthfood.infrastructure.mapper.ProductHealthFoodMapper; import com.webeye.backend.healthfood.infrastructure.persistence.HealthFoodRepository; +import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.product.domain.Product; import com.webeye.backend.product.domain.ProductHealthfood; @@ -37,6 +38,7 @@ public class HealthFoodService { private String serviceKey; private final OpenAiClient openAiClient; + private final ImageUrlExtractor imageUrlExtractor; private final HealthFoodClient healthFoodClient; private final ProductRepository productRepository; private final HealthFoodRepository healthFoodRepository; @@ -80,7 +82,7 @@ public HealthFoodKeywordResponse analyzeAndSaveHealthFood(FoodProductAnalysisReq } public HealthFoodAiResponse analyzeHealthFood(FoodProductAnalysisRequest request) { - return openAiClient.explainHealthFood(request); + return openAiClient.explainHealthFood(imageUrlExtractor.extractImageUrlFromHtml(request.html())); } @Transactional diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 2d652cb..72f1c6f 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -12,7 +12,6 @@ import com.webeye.backend.product.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; -import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.rawmaterial.dto.response.RawMaterialAiResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -56,7 +55,7 @@ public DetailExplanationResponse explainProductDetail(OutlineType outline, List< return callWithStructuredOutput(urls, prompt, DetailExplanationResponse.class); } - public AllergyAiResponse explainAllergy(FoodProductAnalysisRequest request) { + public AllergyAiResponse explainAllergy(List urls) { String system = """ You are an OCR assistant that extracts and detects allergenic ingredients from Korean product label images. Always treat partial matches inside compound words as valid if they contain the full Korean name of an allergen. @@ -82,10 +81,10 @@ public AllergyAiResponse explainAllergy(FoodProductAnalysisRequest request) { """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request.urls(), prompt, AllergyAiResponse.class); + return callWithStructuredOutput(urls, prompt, AllergyAiResponse.class); } - public NutritionAiResponse explainNutrition(FoodProductAnalysisRequest request) { + public NutritionAiResponse explainNutrition(List urls) { String system = """ You are a nutrition description assistant. """; @@ -96,11 +95,11 @@ public NutritionAiResponse explainNutrition(FoodProductAnalysisRequest request) """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request.urls(), prompt, NutritionAiResponse.class); + return callWithStructuredOutput(urls, prompt, NutritionAiResponse.class); } - public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { + public CosmeticResponse explainCosmetic(List urls) { String system = """ You are an expert in identifying harmful cosmetic ingredients based on Korean labels. You always return exact matches based on a predefined Korean-to-English mapping. @@ -140,10 +139,10 @@ public CosmeticResponse explainCosmetic(ProductAnalysisRequest request) { """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request.urls(), prompt, CosmeticResponse.class); + return callWithStructuredOutput(urls, prompt, CosmeticResponse.class); } - public HealthFoodAiResponse explainHealthFood(FoodProductAnalysisRequest request) { + public HealthFoodAiResponse explainHealthFood(List urls) { String system = """ You are a food label OCR expert. Your task is to extract ingredient names from Korean health supplement product images. You must return only a list of ingredient names, in JSON format. Do not summarize or explain anything. @@ -176,7 +175,7 @@ Ignore quantities (mg, %, g), descriptors (함유, 분말), or anything in paren """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(request.urls(), prompt, HealthFoodAiResponse.class); + return callWithStructuredOutput(urls, prompt, HealthFoodAiResponse.class); } public ImageAnalysisResponse explainImage(ImageAnalysisRequest request) { diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index af9ea84..74de755 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -2,6 +2,7 @@ import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; +import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.nutrition.domain.Nutrient; @@ -24,6 +25,7 @@ @Transactional(readOnly = true) public class NutritionService { private final OpenAiClient openAiClient; + private final ImageUrlExtractor imageUrlExtractor; private final RawMaterialService rawMaterialService; private final NutrientRepository nutrientRepository; @@ -36,7 +38,7 @@ public Nutrient findByType(NutrientType type) { @Transactional public void saveProductNutrition(Product product, FoodProductAnalysisRequest request) { - NutritionAiResponse response = openAiClient.explainNutrition(request); + NutritionAiResponse response = openAiClient.explainNutrition(imageUrlExtractor.extractImageUrlFromHtml(request.html())); if (Boolean.TRUE.equals(response.isNutrientIncluded())) { Map nutrientMap = extractNutrientMap(response); diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 01ece55..c1952f5 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -2,6 +2,7 @@ import com.webeye.backend.allergy.application.AllergyService; import com.webeye.backend.allergy.type.AllergyType; +import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; @@ -22,10 +23,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; @Slf4j @Service @@ -36,6 +34,7 @@ public class ProductService { private final NutrientRecommendationService nutrientRecommendationService; private final AllergyService allergyService; private final OpenAiClient openAiClient; + private final ImageUrlExtractor imageUrlExtractor; private final ProductRepository productRepository; @@ -72,29 +71,13 @@ private List getAllergyResponse(Product product, List .toList(); } - private List getNutrientRecommendationResponse(FoodProductAnalysisRequest request, Product product) { + private List getNutrientRecommendationResponse( + FoodProductAnalysisRequest request, Product product) { return nutrientRecommendationService.analyzeNutrientSufficiency(NutrientRecommendationRequest .builder().birthYear(request.birthYear()).gender(request.gender()).product(product).build()); } public DetailExplanationResponse analyzeProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { - return openAiClient.explainProductDetail(outline, extractImageUrlFromHtml(request.html())); - } - - private List extractImageUrlFromHtml(String html) { - Pattern pattern = Pattern.compile("]+src=[\"'](//[^\"']+)[\"']"); - Matcher matcher = pattern.matcher(html); - - List imageUrls = new ArrayList<>(); - - while (matcher.find()) { - String rawUrl = matcher.group(1); - String fullUrl = "https:" + rawUrl; - imageUrls.add(fullUrl); - } - log.info("extracted urls: {}", imageUrls); - log.info("total number of images: {}", imageUrls.size()); - - return imageUrls; + return openAiClient.explainProductDetail(outline, imageUrlExtractor.extractImageUrlFromHtml(request.html())); } } diff --git a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java index fa37516..5afcc00 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java @@ -20,9 +20,9 @@ public record FoodProductAnalysisRequest( @Schema(description = "상품 제목") String title, - @Schema(description = "상품 이미지 URL") - @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") - List urls, + @Schema(description = "상품 상세 정보 HTML") + @NotEmpty(message = "상품 상세 정보의 HTML은 비어있을 수 없습니다.") + String html, @Schema(description = "사용자 출생년도") @NotNull(message = "사용자의 출생연도는 비어있을 수 없습니다.") diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java index 77ae093..23c14cd 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java @@ -4,16 +4,14 @@ import jakarta.validation.constraints.NotEmpty; import lombok.Builder; -import java.util.List; - @Schema(description = "제품 분석") @Builder public record ProductAnalysisRequest( @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") String productId, - @Schema(description = "상품 이미지 URL") - @NotEmpty(message = "이미지 URL 목록은 비어있을 수 없습니다.") - List urls + @Schema(description = "상품 상세 정보 HTML") + @NotEmpty(message = "상품 상세 정보의 HTML은 비어있을 수 없습니다.") + String html ) { } \ No newline at end of file From ea818ae26cc79d028c644ca0de0c090ec097802a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 19:00:29 +0900 Subject: [PATCH 185/309] =?UTF-8?q?#49=20docs:=20swagger=20=EB=8C=80?= =?UTF-8?q?=EA=B4=84=ED=98=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/swagger/ImageAnalysisSwagger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java b/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java index c712a4b..0d48894 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java +++ b/src/main/java/com/webeye/backend/imageanalysis/presentation/swagger/ImageAnalysisSwagger.java @@ -12,7 +12,7 @@ @Tag(name = "[이미지 분석]", description = "이미지 분석 관련 API") public interface ImageAnalysisSwagger { @Operation( - summary = "[이미지 분석]", + summary = "이미지 분석", description = "이미지 URL을 입력받아 이미지를 분석을 제공합니다." ) @ApiResponses(value = { From 50f985dcc8766c3e6cd9965e043cbc30278a2be1 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 17 May 2025 19:00:46 +0900 Subject: [PATCH 186/309] =?UTF-8?q?#50=20docs:=20schema=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/dto/request/ReviewSummaryRequest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 24d5e0c..eac5cd3 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -8,6 +8,21 @@ @Builder @Schema(description = "리뷰 목록") public record ReviewSummaryRequest( + @Schema(description = "리뷰 만족도 통계", + example = """ + { + "맛 만족도": { + "맛있어요": 55, + "보통이에요": 16, + "생각보다 별로예요": 29 + }, + "당도": { + "아주 달콤해요": 39, + "적당해요": 29, + "달지 않아요": 32 + } + } + """) Map> reviews ) { } From a1cb7b1a0ca0ef17c4b9713c7f8a9b8968dfbcdd Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 17 May 2025 19:01:29 +0900 Subject: [PATCH 187/309] =?UTF-8?q?#50=20docs:=20recore=20schema=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/review/dto/request/ReviewSummaryRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index eac5cd3..967f060 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -6,7 +6,7 @@ import java.util.Map; @Builder -@Schema(description = "리뷰 목록") +@Schema(description = "리뷰 만족도") public record ReviewSummaryRequest( @Schema(description = "리뷰 만족도 통계", example = """ From 2d1df2368905c366e7eca4dbe195e61dfb228e3c Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 19:15:20 +0900 Subject: [PATCH 188/309] =?UTF-8?q?#49=20feat:=20html=EC=97=90=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=8F=AC=ED=95=A8=EB=90=98=EC=96=B4=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=EC=A7=80=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/product/dto/request/FoodProductAnalysisRequest.java | 2 ++ .../backend/product/dto/request/ProductAnalysisRequest.java | 2 ++ .../product/dto/request/ProductDetailAnalysisRequest.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java index 5afcc00..cbbcd1f 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import lombok.Builder; import java.util.List; @@ -22,6 +23,7 @@ public record FoodProductAnalysisRequest( @Schema(description = "상품 상세 정보 HTML") @NotEmpty(message = "상품 상세 정보의 HTML은 비어있을 수 없습니다.") + @Pattern(regexp = ".*.*", message = "HTML에는 최소한 하나의 이미지 태그가 포함되어야 합니다.") String html, @Schema(description = "사용자 출생년도") diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java index 23c14cd..6f0a19e 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductAnalysisRequest.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; import lombok.Builder; @Schema(description = "제품 분석") @@ -12,6 +13,7 @@ public record ProductAnalysisRequest( @Schema(description = "상품 상세 정보 HTML") @NotEmpty(message = "상품 상세 정보의 HTML은 비어있을 수 없습니다.") + @Pattern(regexp = ".*.*", message = "HTML에는 최소한 하나의 이미지 태그가 포함되어야 합니다.") String html ) { } \ No newline at end of file diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java index 9646142..748341d 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; import lombok.Builder; @Schema(description = "상품 설명 이미지의 HTML") @@ -9,6 +10,7 @@ public record ProductDetailAnalysisRequest( @Schema(description = "상품 상세 정보 HTML") @NotEmpty(message = "상품 상세 정보의 HTML은 비어있을 수 없습니다.") + @Pattern(regexp = ".*.*", message = "HTML에는 최소한 하나의 이미지 태그가 포함되어야 합니다.") String html ){ } From 79dc80fb22f4bed5262dd02b495834e5db519ca6 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 19:23:03 +0900 Subject: [PATCH 189/309] =?UTF-8?q?#49=20feat:=20=EC=B6=94=EC=B6=9C?= =?UTF-8?q?=EB=90=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20URL=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EC=9D=84=20=EB=95=8C=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/error/ErrorCode.java | 3 +++ .../imageanalysis/infrastructure/ImageUrlExtractor.java | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/com/webeye/backend/global/error/ErrorCode.java b/src/main/java/com/webeye/backend/global/error/ErrorCode.java index a162114..25b9e1d 100644 --- a/src/main/java/com/webeye/backend/global/error/ErrorCode.java +++ b/src/main/java/com/webeye/backend/global/error/ErrorCode.java @@ -14,6 +14,9 @@ public enum ErrorCode { UNSUPPORTED_IMAGE_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 형식입니다."), INVALID_IMAGE_URL(HttpStatus.BAD_REQUEST, "잘못된 이미지 URL입니다."), + // image url extract + IMAGE_URL_NOT_FOUND(HttpStatus.NOT_FOUND, "이미지의 URL이 추출되지 않았습니다."), + // open api OPEN_API_RESPONSE_NULL(HttpStatus.INTERNAL_SERVER_ERROR, "Open API 응답에 실패했습니다."), OPEN_API_DATA_MISSING(HttpStatus.NOT_FOUND, "Open API 데이터가 존재하지 않습니다."), diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java index cf29974..b3edbc2 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -1,5 +1,7 @@ package com.webeye.backend.imageanalysis.infrastructure; +import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.global.error.ErrorCode; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -25,6 +27,9 @@ public List extractImageUrlFromHtml(String html) { log.info("extracted urls: {}", imageUrls); log.info("total number of images: {}", imageUrls.size()); + if (imageUrls.isEmpty()) { + throw new BusinessException(ErrorCode.IMAGE_URL_NOT_FOUND); + } return imageUrls; } } From 641abe3a0a6897c4d9ffe33af84733371e52fa0e Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 17 May 2025 20:10:38 +0900 Subject: [PATCH 190/309] #53 remove: product nutrient unique --- .../com/webeye/backend/product/domain/ProductNutrient.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java index 9be4523..dbe2788 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java +++ b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java @@ -8,9 +8,6 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table( - uniqueConstraints = @UniqueConstraint(columnNames = {"product_id", "nutrient_id"}) -) public class ProductNutrient extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From a4737186621281a23cfeffed802099fb08e6fafe Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 17 May 2025 22:08:42 +0900 Subject: [PATCH 191/309] =?UTF-8?q?#50=20feat:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EC=8B=9C,=20=ED=8F=89=EA=B7=A0=20?= =?UTF-8?q?=EB=B3=84=EC=A0=90=20=EA=B3=84=EC=82=B0=20=ED=9B=84=20=EB=B0=98?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/review/domain/Review.java | 12 +++++- .../dto/request/ReviewSummaryRequest.java | 9 ++++- .../dto/response/ReviewSummaryResponse.java | 2 + .../clovaX/ClovaXClientService.java | 39 +++++++++++++++---- .../infrastructure/mapper/ReviewMapper.java | 1 + 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/domain/Review.java b/src/main/java/com/webeye/backend/review/domain/Review.java index 9db3904..b923d7b 100644 --- a/src/main/java/com/webeye/backend/review/domain/Review.java +++ b/src/main/java/com/webeye/backend/review/domain/Review.java @@ -20,6 +20,9 @@ public class Review extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column + private Double averageRating; + @Column(nullable = false) private String positiveSummary; @@ -34,11 +37,16 @@ public class Review extends BaseEntity { // private Product product; @Builder - public Review(String positiveSummary, String negativeSummary, String keywords) { + public Review( + Double averageRating, + String positiveSummary, + String negativeSummary, + String keywords + ) { + this.averageRating = averageRating; this.positiveSummary = positiveSummary; this.negativeSummary = negativeSummary; this.keywords = keywords; -// this.product = product; } public List getKeywordList() { diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 967f060..9949dc2 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -20,7 +20,14 @@ public record ReviewSummaryRequest( "아주 달콤해요": 39, "적당해요": 29, "달지 않아요": 32 - } + }, + "별점": { + "최고": 83, + "좋음": 11, + "보통": 4, + "별로": 1, + "나쁨": 1 + } } """) Map> reviews diff --git a/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java index 6539e49..54a089f 100644 --- a/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java +++ b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java @@ -8,6 +8,8 @@ @Builder @Schema(description = "리뷰 요약") public record ReviewSummaryResponse( + @Schema(description = "평균 별점", example = "4.85") + double averageRating, @Schema(description = "긍정 리뷰", example = "맛있다는 평가가 많습니다.") String positive, @Schema(description = "부정 리뷰", example = "배송이 느리다는 평가가 많습니다.") diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 7b35913..0da4200 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -11,10 +11,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Service @@ -32,6 +29,11 @@ public class ClovaXClientService { public ReviewSummaryResponse summarizeReviewText(Map> reviewText) { String inputText = convertReviewMapToText(reviewText); + double averageRating = 0.0; + if (reviewText.containsKey("별점")) { + averageRating = calculateAverageRating(reviewText.get("별점")); + } + ClovaXRequest clovaXRequest = new ClovaXRequest(List.of( new ClovaXMessage(Role.SYSTEM, List.of( new ClovaXContent(ContentType.TEXT, @@ -57,10 +59,10 @@ public ReviewSummaryResponse summarizeReviewText(Map> reviewMap) { @@ -94,4 +95,26 @@ private String convertReviewMapToText(Map> reviewMa return sb.toString(); } + private double calculateAverageRating(Map ratings) { + Map ratingPoint = Map.of( + "최고", 5, + "좋음", 4, + "보통", 3, + "별로", 2, + "나쁨", 1 + ); + int totalScore = 0; + int totalPercent = 0; + + for (Map.Entry entry : ratings.entrySet()) { + Integer point = ratingPoint.get(entry.getKey()); + Integer percent = entry.getValue(); + + if (point != null) { + totalScore += point * percent; + totalPercent += percent; + } + } + return totalPercent == 0 ? 0.0 : Math.round((totalScore / (double) totalPercent) * 100.0) / 100.0; + } } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java index 8ac7862..2f1c197 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java @@ -17,6 +17,7 @@ public static Review toEntity(ReviewSummaryResponse response) { public static ReviewSummaryResponse toResponse(Review review) { return new ReviewSummaryResponse( + review.getAverageRating(), review.getPositiveSummary(), review.getNegativeSummary(), Arrays.asList(review.getKeywords().split(",")) From f21c93e909a3cb3286c608a32d7cff3fc53af040 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 17 May 2025 22:36:49 +0900 Subject: [PATCH 192/309] =?UTF-8?q?#50=20refactor:=20=ED=94=84=EB=A1=AC?= =?UTF-8?q?=ED=94=84=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clovaX/ClovaXClientService.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 0da4200..ec50ac0 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -37,20 +37,26 @@ public ReviewSummaryResponse summarizeReviewText(Map Date: Sat, 17 May 2025 22:39:18 +0900 Subject: [PATCH 193/309] =?UTF-8?q?#50=20style:=20=EB=93=A4=EC=97=AC?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clovaX/ClovaXClientService.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index ec50ac0..8997cea 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -38,25 +38,25 @@ public ReviewSummaryResponse summarizeReviewText(Map Date: Sat, 17 May 2025 22:45:52 +0900 Subject: [PATCH 194/309] =?UTF-8?q?#50=20refactor:=20=ED=94=84=EB=A1=AC?= =?UTF-8?q?=ED=94=84=ED=8A=B8=20=EC=9E=AC=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clovaX/ClovaXClientService.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 8997cea..73d1d51 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -44,18 +44,19 @@ public ReviewSummaryResponse summarizeReviewText(Map Date: Sun, 18 May 2025 02:15:23 +0900 Subject: [PATCH 195/309] =?UTF-8?q?#50=20fix:=20=EA=B8=8D=EC=A0=95,=20?= =?UTF-8?q?=EB=B6=80=EC=A0=95=20=EB=A6=AC=EB=B7=B0=20=EC=B5=9C=EB=8C=80=20?= =?UTF-8?q?3=EA=B0=9C=EA=B9=8C=EC=A7=80=20=ED=91=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/ReviewSummaryResponse.java | 7 +++-- .../clovaX/ClovaXClientService.java | 27 ++++++++++++++----- .../infrastructure/mapper/ReviewMapper.java | 8 +++--- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java index 54a089f..ced93a1 100644 --- a/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java +++ b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java @@ -10,10 +10,13 @@ public record ReviewSummaryResponse( @Schema(description = "평균 별점", example = "4.85") double averageRating, + @Schema(description = "긍정 리뷰", example = "맛있다는 평가가 많습니다.") - String positive, + List positiveReviews, + @Schema(description = "부정 리뷰", example = "배송이 느리다는 평가가 많습니다.") - String negative, + List negativeReviews, + @Schema(description = "키워드", example = "맛있어요, 신선해요") List keywords ) { diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 73d1d51..759fb52 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -57,6 +57,7 @@ public ReviewSummaryResponse summarizeReviewText(Map positiveReviews = new ArrayList<>(); + List negativeReviews = new ArrayList<>(); List keywords = new ArrayList<>(); for (String line : lines) { if (line != null && line.startsWith("긍정 리뷰:")) { - positive = line.replace("긍정 리뷰:", "").trim(); + String text = line.replace("긍정 리뷰:", "").trim(); + positiveReviews = Arrays.stream(text.split("[.,]")) + .map(String::trim) + .filter(s -> !s.isBlank()) + .limit(3) + .toList(); } else if (line != null && line.startsWith("부정 리뷰:")) { - negative = line.replace("부정 리뷰:", "").trim(); + String text = line.replace("부정 리뷰:", "").trim(); + negativeReviews = Arrays.stream(text.split("[.,]")) + .map(String::trim) + .filter(s -> !s.isBlank()) + .limit(3) + .toList(); } else if (line != null && line.startsWith("키워드:")) { String[] tokens = line.replace("키워드:", "").split(","); - keywords = Arrays.stream(tokens).map(String::trim).collect(Collectors.toList()); + keywords = Arrays.stream(tokens) + .map(String::trim) + .filter(s -> !s.isBlank()) + .limit(3) + .toList(); } } - return new ReviewSummaryResponse(averageRating, positive, negative, keywords); + return new ReviewSummaryResponse(averageRating, positiveReviews, negativeReviews, keywords); } private String convertReviewMapToText(Map> reviewMap) { diff --git a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java index 2f1c197..f8935db 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java @@ -9,8 +9,8 @@ public class ReviewMapper { public static Review toEntity(ReviewSummaryResponse response) { return Review.builder() - .positiveSummary(response.positive()) - .negativeSummary(response.negative()) + .positiveSummary(String.join("||", response.positiveReviews())) + .negativeSummary(String.join("||", response.negativeReviews())) .keywords(String.join(",", response.keywords())) .build(); } @@ -18,8 +18,8 @@ public static Review toEntity(ReviewSummaryResponse response) { public static ReviewSummaryResponse toResponse(Review review) { return new ReviewSummaryResponse( review.getAverageRating(), - review.getPositiveSummary(), - review.getNegativeSummary(), + Arrays.asList(review.getPositiveSummary().split("\\|\\|")), + Arrays.asList(review.getNegativeSummary().split("\\|\\|")), Arrays.asList(review.getKeywords().split(",")) ); } From 8c0ee63448f6d2a44ef3cefbd602dae92cc27663 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 18 May 2025 02:48:10 +0900 Subject: [PATCH 196/309] =?UTF-8?q?#50=20feat:=20review,=20product=20?= =?UTF-8?q?=EC=9D=BC=EB=8C=80=EC=9D=BC=EA=B4=80=EA=B3=84=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/product/domain/Product.java | 12 +++++++++++- .../com/webeye/backend/review/domain/Review.java | 12 +++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 9f8df9d..2da070a 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -1,6 +1,7 @@ package com.webeye.backend.product.domain; import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.review.domain.Review; import jakarta.persistence.*; import lombok.*; @@ -15,6 +16,9 @@ public class Product extends BaseEntity { @Column(name = "product_id", nullable = false) private String id; // 쿠팡에서 products 뒤에 오는 숫자 + @OneToOne(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + private Review review; + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) private List allergies = new ArrayList<>(); @@ -25,8 +29,14 @@ public class Product extends BaseEntity { private List healthFoods = new ArrayList<>(); @Builder - public Product(String id) { + public Product(String id, Review review) { this.id = id; + this.review = review; + } + + public void associateWithReview(Review review) { + this.review = review; + review.associateWithProduct(this); } public void addNutrient(ProductNutrient nutrient) { diff --git a/src/main/java/com/webeye/backend/review/domain/Review.java b/src/main/java/com/webeye/backend/review/domain/Review.java index b923d7b..70837f7 100644 --- a/src/main/java/com/webeye/backend/review/domain/Review.java +++ b/src/main/java/com/webeye/backend/review/domain/Review.java @@ -1,6 +1,7 @@ package com.webeye.backend.review.domain; import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.product.domain.Product; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -32,9 +33,9 @@ public class Review extends BaseEntity { @Column(nullable = false) private String keywords; -// @OneToOne(fetch = FetchType.LAZY) -// @JoinColumn(name = "product_id", unique = true) -// private Product product; + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", unique = true) + private Product product; @Builder public Review( @@ -49,6 +50,11 @@ public Review( this.keywords = keywords; } + public void associateWithProduct(Product product) { + this.product = product; + product.associateWithReview(this); + } + public List getKeywordList() { return Arrays.asList(this.keywords.split(",")); } From 9527fd864ef361d4b0310a1b6f1e96ec99d3b694 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 18 May 2025 04:10:39 +0900 Subject: [PATCH 197/309] =?UTF-8?q?#50=20feat:=20request=EC=97=90=20produc?= =?UTF-8?q?tId=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/product/domain/Product.java | 7 ++++--- .../backend/review/application/ReviewService.java | 14 +++++++++++++- .../com/webeye/backend/review/domain/Review.java | 4 +++- .../review/dto/request/ReviewSummaryRequest.java | 3 +++ .../review/infrastructure/mapper/ReviewMapper.java | 8 ++++++-- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 2da070a..1e86ef4 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -29,14 +29,15 @@ public class Product extends BaseEntity { private List healthFoods = new ArrayList<>(); @Builder - public Product(String id, Review review) { + public Product(String id) { this.id = id; - this.review = review; } public void associateWithReview(Review review) { this.review = review; - review.associateWithProduct(this); + if (review.getProduct() != this) { + review.associateWithProduct(this); + } } public void addNutrient(ProductNutrient nutrient) { diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index dfdcc30..28d9cba 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -1,5 +1,9 @@ package com.webeye.backend.review.application; +import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.global.error.ErrorCode; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.persistent.ProductRepository; import com.webeye.backend.review.domain.Review; import com.webeye.backend.review.dto.request.ReviewSummaryRequest; import com.webeye.backend.review.dto.response.ReviewSummaryResponse; @@ -18,14 +22,22 @@ public class ReviewService { private final ClovaXClientService clovaXClientService; private final ReviewRepository reviewRepository; + private final ProductRepository productRepository; @Transactional public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { + Product product = Product.builder() + .id(request.productId()) + .build(); + + productRepository.save(product); + Map> reviewMap = request.reviews(); ReviewSummaryResponse response = clovaXClientService.summarizeReviewText(reviewMap); - Review review = ReviewMapper.toEntity(response); + Review review = ReviewMapper.toEntity(response, product); + reviewRepository.save(review); return response; diff --git a/src/main/java/com/webeye/backend/review/domain/Review.java b/src/main/java/com/webeye/backend/review/domain/Review.java index 70837f7..e9da463 100644 --- a/src/main/java/com/webeye/backend/review/domain/Review.java +++ b/src/main/java/com/webeye/backend/review/domain/Review.java @@ -52,7 +52,9 @@ public Review( public void associateWithProduct(Product product) { this.product = product; - product.associateWithReview(this); + if (product.getReview() != this) { + product.associateWithReview(this); + } } public List getKeywordList() { diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 9949dc2..281f3f3 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -8,6 +8,9 @@ @Builder @Schema(description = "리뷰 만족도") public record ReviewSummaryRequest( + @Schema(description = "쿠팡 상품 ID", example = "85241789") + String productId, + @Schema(description = "리뷰 만족도 통계", example = """ { diff --git a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java index f8935db..1ecc3c9 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java @@ -1,5 +1,6 @@ package com.webeye.backend.review.infrastructure.mapper; +import com.webeye.backend.product.domain.Product; import com.webeye.backend.review.domain.Review; import com.webeye.backend.review.dto.response.ReviewSummaryResponse; @@ -7,12 +8,15 @@ public class ReviewMapper { - public static Review toEntity(ReviewSummaryResponse response) { - return Review.builder() + public static Review toEntity(ReviewSummaryResponse response, Product product) { + Review review = Review.builder() .positiveSummary(String.join("||", response.positiveReviews())) .negativeSummary(String.join("||", response.negativeReviews())) .keywords(String.join(",", response.keywords())) .build(); + review.associateWithProduct(product); + + return review; } public static ReviewSummaryResponse toResponse(Review review) { From 319283de1ac0c80abeff25374b0f51c893ac4d88 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 18 May 2025 17:36:50 +0900 Subject: [PATCH 198/309] =?UTF-8?q?#50=20fix:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/review/application/ReviewService.java | 13 +++++++------ .../review/infrastructure/mapper/ReviewMapper.java | 1 + 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 28d9cba..4230246 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -26,12 +26,13 @@ public class ReviewService { @Transactional public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { - Product product = Product.builder() - .id(request.productId()) - .build(); - - productRepository.save(product); - + Product product = productRepository.findById(request.productId()) + .orElseGet(() -> { + Product newProduct = Product.builder() + .id(request.productId()) + .build(); + return productRepository.save(newProduct); + }); Map> reviewMap = request.reviews(); ReviewSummaryResponse response = clovaXClientService.summarizeReviewText(reviewMap); diff --git a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java index 1ecc3c9..8122cd5 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java @@ -10,6 +10,7 @@ public class ReviewMapper { public static Review toEntity(ReviewSummaryResponse response, Product product) { Review review = Review.builder() + .averageRating(response.averageRating()) .positiveSummary(String.join("||", response.positiveReviews())) .negativeSummary(String.join("||", response.negativeReviews())) .keywords(String.join(",", response.keywords())) From b93419fa2ad64a56f2b1cead3782bfd9fafd64d6 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 18 May 2025 17:37:57 +0900 Subject: [PATCH 199/309] =?UTF-8?q?#50=20chor:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20import=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/review/application/ReviewService.java | 2 -- .../review/infrastructure/clovaX/ClovaXClientService.java | 1 - 2 files changed, 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 4230246..86e9d2a 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -1,7 +1,5 @@ package com.webeye.backend.review.application; -import com.webeye.backend.global.error.BusinessException; -import com.webeye.backend.global.error.ErrorCode; import com.webeye.backend.product.domain.Product; import com.webeye.backend.product.persistent.ProductRepository; import com.webeye.backend.review.domain.Review; diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 759fb52..d9360b3 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -12,7 +12,6 @@ import org.springframework.stereotype.Service; import java.util.*; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor From dcf6ed80d58f008c9fe730e78312cca0c8a86400 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 18 May 2025 17:42:25 +0900 Subject: [PATCH 200/309] =?UTF-8?q?#50=20feat:=20avergeRating=20nullable?= =?UTF-8?q?=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/review/domain/Review.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/review/domain/Review.java b/src/main/java/com/webeye/backend/review/domain/Review.java index e9da463..c7e3e7a 100644 --- a/src/main/java/com/webeye/backend/review/domain/Review.java +++ b/src/main/java/com/webeye/backend/review/domain/Review.java @@ -21,7 +21,7 @@ public class Review extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column + @Column(nullable = false) private Double averageRating; @Column(nullable = false) From be21e09b3afb694139b09abe8e04905a49f05af6 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 20 May 2025 18:08:57 +0900 Subject: [PATCH 201/309] =?UTF-8?q?#50=20fix:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 7 ++-- .../dto/request/ReviewSummaryRequest.java | 32 +++++++++++------ .../clovaX/ClovaXClientService.java | 36 ++++++++----------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 86e9d2a..6530f78 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -31,9 +31,12 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { .build(); return productRepository.save(newProduct); }); - Map> reviewMap = request.reviews(); - ReviewSummaryResponse response = clovaXClientService.summarizeReviewText(reviewMap); + ReviewSummaryResponse response = clovaXClientService.summarizeReviewText( + request.reviews(), + request.reviewRating().ratings().get("별점"), + request.reviewRating().totalCount() + ); Review review = ReviewMapper.toEntity(response, product); diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 281f3f3..53cba59 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -11,8 +11,10 @@ public record ReviewSummaryRequest( @Schema(description = "쿠팡 상품 ID", example = "85241789") String productId, - @Schema(description = "리뷰 만족도 통계", - example = """ + @Schema(description = "별점 통계") + ReviewRating reviewRating, + + @Schema(description = "리뷰 만족도 통계", example = """ { "맛 만족도": { "맛있어요": 55, @@ -23,16 +25,26 @@ public record ReviewSummaryRequest( "아주 달콤해요": 39, "적당해요": 29, "달지 않아요": 32 - }, - "별점": { - "최고": 83, - "좋음": 11, - "보통": 4, - "별로": 1, - "나쁨": 1 - } + } } """) Map> reviews ) { + public record ReviewRating( + @Schema(description = "총 별점 수", example = "150") + int totalCount, + + @Schema(description = "별점 등급별 수", example = """ + { + "별점": { + "최고": 83, + "좋음": 11, + "보통": 4, + "별로": 1, + "나쁨": 1 + } + } + """) + Map> ratings + ){} } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index d9360b3..6eb581b 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -25,14 +25,10 @@ public class ClovaXClientService { @Value("${clova.request-id}") private String requestId; - public ReviewSummaryResponse summarizeReviewText(Map> reviewText) { + public ReviewSummaryResponse summarizeReviewText(Map> reviewText, Map ratingMap, int totalCount) { + double averageRating = calculateAverageRating(ratingMap, totalCount); String inputText = convertReviewMapToText(reviewText); - double averageRating = 0.0; - if (reviewText.containsKey("별점")) { - averageRating = calculateAverageRating(reviewText.get("별점")); - } - ClovaXRequest clovaXRequest = new ClovaXRequest(List.of( new ClovaXMessage(Role.SYSTEM, List.of( new ClovaXContent(ContentType.TEXT, @@ -55,8 +51,8 @@ public ReviewSummaryResponse summarizeReviewText(Map> reviewMa return sb.toString(); } - private double calculateAverageRating(Map ratings) { + private double calculateAverageRating(Map ratings, int totalCount) { Map ratingPoint = Map.of( "최고", 5, "좋음", 4, @@ -124,18 +120,14 @@ private double calculateAverageRating(Map ratings) { "별로", 2, "나쁨", 1 ); - int totalScore = 0; - int totalPercent = 0; - - for (Map.Entry entry : ratings.entrySet()) { - Integer point = ratingPoint.get(entry.getKey()); - Integer percent = entry.getValue(); - - if (point != null) { - totalScore += point * percent; - totalPercent += percent; - } - } - return totalPercent == 0 ? 0.0 : Math.round((totalScore / (double) totalPercent) * 100.0) / 100.0; + int totalScore = ratings.entrySet().stream() + .mapToInt(entry -> { + Integer rating = ratingPoint.get(entry.getKey()); + Integer score = entry.getValue(); + return rating != null ? rating * score : 0; + }) + .sum(); + + return totalCount == 0 ? 0.0 : Math.round((totalScore / (double) totalCount) * 100.0) / 100.0; } } From 5b7fb9083a42f503b20b67c73126f188bd7bf5db Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 20 May 2025 18:26:32 +0900 Subject: [PATCH 202/309] =?UTF-8?q?#50=20feat:=20=EC=9D=B4=EB=AF=B8=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=9A=94=EC=95=BD=EC=9D=84=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=ED=95=9C=20=EC=83=81=ED=92=88=EC=9D=BC=20=EC=8B=9C,?= =?UTF-8?q?=20DB=EC=97=90=EC=84=9C=20=EC=A1=B0=ED=9A=8C=20=ED=9B=84=20?= =?UTF-8?q?=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/persistent/ProductRepository.java | 7 ++++++ .../review/application/ReviewService.java | 25 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java index 4254ef2..5b49fa5 100644 --- a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java +++ b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java @@ -2,9 +2,16 @@ import com.webeye.backend.product.domain.Product; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface ProductRepository extends JpaRepository { boolean existsById(String id); + + @Query("SELECT p FROM Product p LEFT JOIN FETCH p.review WHERE p.id = :id") + Optional findByIdWithReview(@Param("id") String id); } diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 6530f78..a0bdfd0 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -12,8 +12,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Map; - @Service @RequiredArgsConstructor public class ReviewService { @@ -24,20 +22,18 @@ public class ReviewService { @Transactional public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { - Product product = productRepository.findById(request.productId()) - .orElseGet(() -> { - Product newProduct = Product.builder() - .id(request.productId()) - .build(); - return productRepository.save(newProduct); - }); + Product product = findOrCreateProduct(request.productId()); + + Review existingReview = product.getReview(); + if (existingReview != null) { + return ReviewMapper.toResponse(existingReview); + } ReviewSummaryResponse response = clovaXClientService.summarizeReviewText( request.reviews(), request.reviewRating().ratings().get("별점"), request.reviewRating().totalCount() ); - Review review = ReviewMapper.toEntity(response, product); reviewRepository.save(review); @@ -45,5 +41,12 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { return response; } - // TODO: 저장된 리뷰 요약 조회 + @Transactional + public Product findOrCreateProduct(String productId) { + return productRepository.findByIdWithReview(productId) + .orElseGet(() -> productRepository.save( + Product.builder() + .id(productId) + .build())); + } } From 7f574255000fbe6b4c8238c7b7aea189a099b9d4 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 20 May 2025 20:45:43 +0900 Subject: [PATCH 203/309] =?UTF-8?q?#50=20fix:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 2 +- .../review/dto/request/ReviewSummaryRequest.java | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index a0bdfd0..321311f 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -31,7 +31,7 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { ReviewSummaryResponse response = clovaXClientService.summarizeReviewText( request.reviews(), - request.reviewRating().ratings().get("별점"), + request.reviewRating().ratings(), request.reviewRating().totalCount() ); Review review = ReviewMapper.toEntity(response, product); diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 53cba59..54c6dd2 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -36,15 +36,13 @@ public record ReviewRating( @Schema(description = "별점 등급별 수", example = """ { - "별점": { - "최고": 83, - "좋음": 11, - "보통": 4, - "별로": 1, - "나쁨": 1 - } - } + "최고": 83, + "좋음": 11, + "보통": 4, + "별로": 1, + "나쁨": 1 + } """) - Map> ratings + Map ratings ){} } From 983b97d94560e6ea03960da0dd513b3fe9e69201 Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 21 May 2025 20:14:47 +0900 Subject: [PATCH 204/309] =?UTF-8?q?#59=20refactor:=20response=EC=97=90=20t?= =?UTF-8?q?otalCount=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/review/application/ReviewService.java | 2 +- .../backend/review/dto/response/ReviewSummaryResponse.java | 3 +++ .../review/infrastructure/clovaX/ClovaXClientService.java | 6 +++--- .../backend/review/infrastructure/mapper/ReviewMapper.java | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 321311f..db4b75f 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -26,7 +26,7 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { Review existingReview = product.getReview(); if (existingReview != null) { - return ReviewMapper.toResponse(existingReview); + return ReviewMapper.toResponse(existingReview, request.reviewRating().totalCount()); } ReviewSummaryResponse response = clovaXClientService.summarizeReviewText( diff --git a/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java index ced93a1..2ad1e8d 100644 --- a/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java +++ b/src/main/java/com/webeye/backend/review/dto/response/ReviewSummaryResponse.java @@ -8,6 +8,9 @@ @Builder @Schema(description = "리뷰 요약") public record ReviewSummaryResponse( + @Schema(description = "총 리뷰 수", example = "45078") + int totalCount, + @Schema(description = "평균 별점", example = "4.85") double averageRating, diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 6eb581b..648c5ed 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -62,10 +62,10 @@ public ReviewSummaryResponse summarizeReviewText(Map positiveReviews = new ArrayList<>(); @@ -96,7 +96,7 @@ private ReviewSummaryResponse parseResponse(String content, double averageRating .toList(); } } - return new ReviewSummaryResponse(averageRating, positiveReviews, negativeReviews, keywords); + return new ReviewSummaryResponse(totalCount, averageRating, positiveReviews, negativeReviews, keywords); } private String convertReviewMapToText(Map> reviewMap) { diff --git a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java index 8122cd5..8773cb2 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java @@ -20,8 +20,9 @@ public static Review toEntity(ReviewSummaryResponse response, Product product) { return review; } - public static ReviewSummaryResponse toResponse(Review review) { + public static ReviewSummaryResponse toResponse(Review review, int totalCount) { return new ReviewSummaryResponse( + totalCount, review.getAverageRating(), Arrays.asList(review.getPositiveSummary().split("\\|\\|")), Arrays.asList(review.getNegativeSummary().split("\\|\\|")), From 5eea062daf221f3131d3c8ed8fb0dee998d5f034 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 22 May 2025 16:48:55 +0900 Subject: [PATCH 205/309] =?UTF-8?q?#61=20fix:=20=EB=94=94=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=ED=94=84=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../imageanalysis/infrastructure/ImageUrlExtractor.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java index b3edbc2..e072401 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -2,6 +2,7 @@ import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; +import groovy.json.StringEscapeUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -14,6 +15,8 @@ @Component public class ImageUrlExtractor { public List extractImageUrlFromHtml(String html) { + html = StringEscapeUtils.unescapeJava(html); + Pattern pattern = Pattern.compile("]+src=[\"'](//[^\"']+)[\"']"); Matcher matcher = pattern.matcher(html); From b39042746f48387e613b8b7c706f0d7527f03a5b Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 22 May 2025 22:56:26 +0900 Subject: [PATCH 206/309] #63 fix: product nutrient duplicate remove remove product.getNutrients().add(this) of associateWithProduct --- .../java/com/webeye/backend/product/domain/ProductNutrient.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java index dbe2788..f6779b3 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java +++ b/src/main/java/com/webeye/backend/product/domain/ProductNutrient.java @@ -33,6 +33,5 @@ public ProductNutrient(Product product, Nutrient nutrient, Double amount) { public void associateWithProduct(Product product) { this.product = product; - product.getNutrients().add(this); } } \ No newline at end of file From e65867de6a219dfba409515d1ffa55882d1e20b8 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 23 May 2025 16:46:33 +0900 Subject: [PATCH 207/309] =?UTF-8?q?#65=20feat:=20product=5Ftype=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/webeye/backend/product/domain/Product.java | 8 +++++++- .../webeye/backend/product/domain/type/ProductType.java | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/webeye/backend/product/domain/type/ProductType.java diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 1e86ef4..f05f9a5 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -1,6 +1,7 @@ package com.webeye.backend.product.domain; import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.product.domain.type.ProductType; import com.webeye.backend.review.domain.Review; import jakarta.persistence.*; import lombok.*; @@ -16,6 +17,10 @@ public class Product extends BaseEntity { @Column(name = "product_id", nullable = false) private String id; // 쿠팡에서 products 뒤에 오는 숫자 + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ProductType productType; + @OneToOne(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private Review review; @@ -29,8 +34,9 @@ public class Product extends BaseEntity { private List healthFoods = new ArrayList<>(); @Builder - public Product(String id) { + public Product(String id, ProductType productType) { this.id = id; + this.productType = productType; } public void associateWithReview(Review review) { diff --git a/src/main/java/com/webeye/backend/product/domain/type/ProductType.java b/src/main/java/com/webeye/backend/product/domain/type/ProductType.java new file mode 100644 index 0000000..c7aeba7 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/domain/type/ProductType.java @@ -0,0 +1,7 @@ +package com.webeye.backend.product.domain.type; + +public enum ProductType { + FOOD, + COSMETIC, + HEALTH_FOOD +} From e995eb2fd6b9d250ef38a7ae0865d54845e12bda Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 23 May 2025 18:09:19 +0900 Subject: [PATCH 208/309] =?UTF-8?q?#65=20feat:=20cosmetic=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20domain=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/cosmetic/domain/Cosmetic.java | 30 ++++++++++++++ .../cosmetic/domain/CosmeticIngredient.java | 39 +++++++++++++++++++ .../backend/cosmetic/domain/Ingredient.java | 33 ++++++++++++++++ .../cosmetic/domain/type/IngredientType.java | 33 ++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 src/main/java/com/webeye/backend/cosmetic/domain/Cosmetic.java create mode 100644 src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java create mode 100644 src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java create mode 100644 src/main/java/com/webeye/backend/cosmetic/domain/type/IngredientType.java diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/Cosmetic.java b/src/main/java/com/webeye/backend/cosmetic/domain/Cosmetic.java new file mode 100644 index 0000000..23423d5 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/domain/Cosmetic.java @@ -0,0 +1,30 @@ +package com.webeye.backend.cosmetic.domain; + +import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.product.domain.Product; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Cosmetic extends BaseEntity { + + @Id + @Column(name = "cosmetic_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", unique = true) + private Product product; + + @Builder + public Cosmetic(Long id, Product product) { + this.id = id; + this.product = product; + } +} diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java b/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java new file mode 100644 index 0000000..2fdf366 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java @@ -0,0 +1,39 @@ +package com.webeye.backend.cosmetic.domain; + +import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.product.domain.Product; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CosmeticIngredient extends BaseEntity { + + @Id + @Column(name = "cosmetic_ingredient_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cosmetic_id") + private Cosmetic cosmetic; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Builder + public CosmeticIngredient( + Long id, + Cosmetic cosmetic, + Product product + ) { + this.id = id; + this.cosmetic = cosmetic; + this.product = product; + } +} diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java b/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java new file mode 100644 index 0000000..e211a19 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java @@ -0,0 +1,33 @@ +package com.webeye.backend.cosmetic.domain; + +import com.webeye.backend.cosmetic.domain.type.IngredientType; +import com.webeye.backend.global.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Ingredient extends BaseEntity { + + @Id + @Column(name = "ingredient_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, unique = true) + private IngredientType ingredientType; + + @Builder + public Ingredient( + Long id, + IngredientType ingredientType + ) { + this.id = id; + this.ingredientType = ingredientType; + } +} diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/type/IngredientType.java b/src/main/java/com/webeye/backend/cosmetic/domain/type/IngredientType.java new file mode 100644 index 0000000..7d34441 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/domain/type/IngredientType.java @@ -0,0 +1,33 @@ +package com.webeye.backend.cosmetic.domain.type; + +import lombok.Getter; + +@Getter +public enum IngredientType { + AVOBENZONE("아보벤존"), + ISOPROPYL_ALCOHOL("이소프로필 알코올"), + SODIUM_LAURYL_SULFATE("소듐 라우릴/라우레스 설페이트 (SLS, SLES)"), + TRIETHANOLAMINE("트리에탄올아민"), + POLYETHYLENE_GLYCOL("폴리에틸렌 글라이콜 (PEGs)"), + SYNTHETIC_COLORANT("합성 착색료"), + ISOPROPYL_METHYLPHENOL("이소프로필 메틸페놀"), + SORBIC_ACID("소르빅 애씨드"), + HORMONE("호르몬류"), + DIBUTYL_HYDROXYTOLUENE("디부틸 하이드록시 톨루엔 (BHT)"), + PARABENS("파라벤류 (Methyl-, Ethyl-, Propylparaben 등)"), + TRICLOSAN("트리클로산"), + BUTYLATED_HYDROXYANISOLE("부틸 하이드록시아니솔 (BHA)"), + OXYBENZONE("옥시벤존"), + IMIDAZOLIDINYL_UREA("이미다졸리디닐 우레아, 디아졸리디닐 우레아, DMDM 하이단토인 등"), + MINERAL_OIL("미네랄 오일, 파라핀오일"), + THYMOL("티몰"), + TRIISOPROPANOLAMINE("트라이아이소프로판올아민"), + SYNTHETIC_FRAGRANCE("인공 향료 (Synthetic Fragrance, Parfum)"), + PHENOXYETHANOL("페녹시에탄올"); + + private final String description; + + IngredientType(String description) { + this.description = description; + } +} From e0df20a11df2cbbea030c8f82ed852df6d167f80 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 23 May 2025 18:12:41 +0900 Subject: [PATCH 209/309] =?UTF-8?q?#65=20feat:=20repository=20=EC=84=A4?= =?UTF-8?q?=E3=84=B9=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cosmetic/persistence/CosmeticIngredientRepository.java | 7 +++++++ .../backend/cosmetic/persistence/CosmeticRepository.java | 7 +++++++ .../backend/cosmetic/persistence/IngredientRepository.java | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticIngredientRepository.java create mode 100644 src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticRepository.java create mode 100644 src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java diff --git a/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticIngredientRepository.java b/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticIngredientRepository.java new file mode 100644 index 0000000..3302976 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticIngredientRepository.java @@ -0,0 +1,7 @@ +package com.webeye.backend.cosmetic.persistence; + +import com.webeye.backend.cosmetic.domain.CosmeticIngredient; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CosmeticIngredientRepository extends JpaRepository { +} diff --git a/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticRepository.java b/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticRepository.java new file mode 100644 index 0000000..e6665d6 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticRepository.java @@ -0,0 +1,7 @@ +package com.webeye.backend.cosmetic.persistence; + +import com.webeye.backend.cosmetic.domain.Cosmetic; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CosmeticRepository extends JpaRepository { +} diff --git a/src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java b/src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java new file mode 100644 index 0000000..be179f7 --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java @@ -0,0 +1,7 @@ +package com.webeye.backend.cosmetic.persistence; + +import com.webeye.backend.cosmetic.domain.Ingredient; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IngredientRepository extends JpaRepository { +} From 713b0cd84b4c9f7791c065d6dade6f67030185c6 Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 23 May 2025 18:20:23 +0900 Subject: [PATCH 210/309] =?UTF-8?q?#65=20feat:=20ingredientInit=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/IngredientRepository.java | 4 +++ .../persistence/init/IngredientInit.java | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/main/java/com/webeye/backend/cosmetic/persistence/init/IngredientInit.java diff --git a/src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java b/src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java index be179f7..6c684b9 100644 --- a/src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java +++ b/src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java @@ -1,7 +1,11 @@ package com.webeye.backend.cosmetic.persistence; import com.webeye.backend.cosmetic.domain.Ingredient; +import com.webeye.backend.cosmetic.domain.type.IngredientType; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface IngredientRepository extends JpaRepository { + Optional findByIngredientType(IngredientType ingredientType); } diff --git a/src/main/java/com/webeye/backend/cosmetic/persistence/init/IngredientInit.java b/src/main/java/com/webeye/backend/cosmetic/persistence/init/IngredientInit.java new file mode 100644 index 0000000..3a8943c --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/persistence/init/IngredientInit.java @@ -0,0 +1,34 @@ +package com.webeye.backend.cosmetic.persistence.init; + +import com.webeye.backend.cosmetic.domain.Ingredient; +import com.webeye.backend.cosmetic.domain.type.IngredientType; +import com.webeye.backend.cosmetic.persistence.IngredientRepository; +import com.webeye.backend.global.util.DummyDataInit; +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; + +@Slf4j +@RequiredArgsConstructor +@Order(1) +@DummyDataInit +public class IngredientInit implements ApplicationRunner { + + private final IngredientRepository ingredientRepository; + + @Builder + public void run(ApplicationArguments args) { + for (IngredientType ingredientType : IngredientType.values()) { + if (ingredientRepository.findByIngredientType(ingredientType).isEmpty()) { + Ingredient ingredient = Ingredient.builder() + .ingredientType(ingredientType) + .build(); + + ingredientRepository.save(ingredient); + } + } + } +} From 8c28303d3396c509e0c2de09c98797b0bef9821e Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 23 May 2025 23:03:50 +0900 Subject: [PATCH 211/309] =?UTF-8?q?#65=20feat:=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=A4=91=EA=B0=84=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CosmeticServiceImpl.java | 23 +++++++++++++++++++ .../CosmeticIngredientRepository.java | 2 +- .../persistence/CosmeticRepository.java | 2 +- .../persistence/IngredientRepository.java | 2 +- .../persistence/init/IngredientInit.java | 4 ++-- .../backend/product/domain/Product.java | 7 +++++- .../product/persistent/ProductRepository.java | 3 +++ 7 files changed, 37 insertions(+), 6 deletions(-) rename src/main/java/com/webeye/backend/cosmetic/{ => infrastructure}/persistence/CosmeticIngredientRepository.java (77%) rename src/main/java/com/webeye/backend/cosmetic/{ => infrastructure}/persistence/CosmeticRepository.java (75%) rename src/main/java/com/webeye/backend/cosmetic/{ => infrastructure}/persistence/IngredientRepository.java (85%) rename src/main/java/com/webeye/backend/cosmetic/{ => infrastructure}/persistence/init/IngredientInit.java (87%) diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java index 46e0c65..45a20ef 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java @@ -1,11 +1,16 @@ package com.webeye.backend.cosmetic.application; +import com.webeye.backend.cosmetic.domain.Cosmetic; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.domain.type.ProductType; import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.persistent.ProductRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -13,9 +18,27 @@ public class CosmeticServiceImpl implements CosmeticService { private final OpenAiClient openAiClient; private final ImageUrlExtractor imageUrlExtractor; + private final ProductRepository productRepository; @Override + @Transactional public CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request) { + Product product = findOrCreateProduct(request.productId()); + + Cosmetic existingCosmetic = product.getCosmetic(); + if (existingCosmetic != null) { + return null; + } return openAiClient.explainCosmetic(imageUrlExtractor.extractImageUrlFromHtml(request.html())); } + + @Transactional + public Product findOrCreateProduct(String productId) { + return productRepository.findByIdWithCosmetic(productId) + .orElseGet(() -> productRepository.save( + Product.builder() + .id(productId) + .productType(ProductType.COSMETIC) + .build())); + } } diff --git a/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticIngredientRepository.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticIngredientRepository.java similarity index 77% rename from src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticIngredientRepository.java rename to src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticIngredientRepository.java index 3302976..1217443 100644 --- a/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticIngredientRepository.java +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticIngredientRepository.java @@ -1,4 +1,4 @@ -package com.webeye.backend.cosmetic.persistence; +package com.webeye.backend.cosmetic.infrastructure.persistence; import com.webeye.backend.cosmetic.domain.CosmeticIngredient; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticRepository.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticRepository.java similarity index 75% rename from src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticRepository.java rename to src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticRepository.java index e6665d6..2401252 100644 --- a/src/main/java/com/webeye/backend/cosmetic/persistence/CosmeticRepository.java +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticRepository.java @@ -1,4 +1,4 @@ -package com.webeye.backend.cosmetic.persistence; +package com.webeye.backend.cosmetic.infrastructure.persistence; import com.webeye.backend.cosmetic.domain.Cosmetic; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/IngredientRepository.java similarity index 85% rename from src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java rename to src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/IngredientRepository.java index 6c684b9..5fa0fdc 100644 --- a/src/main/java/com/webeye/backend/cosmetic/persistence/IngredientRepository.java +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/IngredientRepository.java @@ -1,4 +1,4 @@ -package com.webeye.backend.cosmetic.persistence; +package com.webeye.backend.cosmetic.infrastructure.persistence; import com.webeye.backend.cosmetic.domain.Ingredient; import com.webeye.backend.cosmetic.domain.type.IngredientType; diff --git a/src/main/java/com/webeye/backend/cosmetic/persistence/init/IngredientInit.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/init/IngredientInit.java similarity index 87% rename from src/main/java/com/webeye/backend/cosmetic/persistence/init/IngredientInit.java rename to src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/init/IngredientInit.java index 3a8943c..1c03db8 100644 --- a/src/main/java/com/webeye/backend/cosmetic/persistence/init/IngredientInit.java +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/init/IngredientInit.java @@ -1,8 +1,8 @@ -package com.webeye.backend.cosmetic.persistence.init; +package com.webeye.backend.cosmetic.infrastructure.persistence.init; import com.webeye.backend.cosmetic.domain.Ingredient; import com.webeye.backend.cosmetic.domain.type.IngredientType; -import com.webeye.backend.cosmetic.persistence.IngredientRepository; +import com.webeye.backend.cosmetic.infrastructure.persistence.IngredientRepository; import com.webeye.backend.global.util.DummyDataInit; import lombok.Builder; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index f05f9a5..66a2c65 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -1,5 +1,6 @@ package com.webeye.backend.product.domain; +import com.webeye.backend.cosmetic.domain.Cosmetic; import com.webeye.backend.global.domain.BaseEntity; import com.webeye.backend.product.domain.type.ProductType; import com.webeye.backend.review.domain.Review; @@ -24,6 +25,9 @@ public class Product extends BaseEntity { @OneToOne(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private Review review; + @OneToOne(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + private Cosmetic cosmetic; + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) private List allergies = new ArrayList<>(); @@ -34,9 +38,10 @@ public class Product extends BaseEntity { private List healthFoods = new ArrayList<>(); @Builder - public Product(String id, ProductType productType) { + public Product(String id, ProductType productType, Cosmetic cosmetic) { this.id = id; this.productType = productType; + this.cosmetic = cosmetic; } public void associateWithReview(Review review) { diff --git a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java index 5b49fa5..5d147e5 100644 --- a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java +++ b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java @@ -14,4 +14,7 @@ public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p LEFT JOIN FETCH p.review WHERE p.id = :id") Optional findByIdWithReview(@Param("id") String id); + + @Query("SELECT p FROM Product p LEFT JOIN p.cosmetic WHERE p.id = :id") + Optional findByIdWithCosmetic(@Param("id") String id); } From ab3acf337283bb5ae020552a16f54d3e5cbdd62b Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 23 May 2025 23:21:16 +0900 Subject: [PATCH 212/309] =?UTF-8?q?#66=20feat:=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clovaX/ClovaXClientService.java | 6 ++++++ .../backend/review/presentation/ReviewController.java | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 648c5ed..da2ee49 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -8,11 +8,13 @@ import com.webeye.backend.review.infrastructure.clovaX.model.ContentType; import com.webeye.backend.review.infrastructure.clovaX.model.Role; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.*; +@Slf4j @Service @RequiredArgsConstructor public class ClovaXClientService { @@ -124,10 +126,14 @@ private double calculateAverageRating(Map ratings, int totalCou .mapToInt(entry -> { Integer rating = ratingPoint.get(entry.getKey()); Integer score = entry.getValue(); + return rating != null ? rating * score : 0; }) .sum(); + log.info("[평균 별점 계산] ratings: {}", ratings); + log.info("[평균 별점 계산] totalCount: {}", totalCount); + return totalCount == 0 ? 0.0 : Math.round((totalScore / (double) totalCount) * 100.0) / 100.0; } } diff --git a/src/main/java/com/webeye/backend/review/presentation/ReviewController.java b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java index 5c1b3c5..3e56dd0 100644 --- a/src/main/java/com/webeye/backend/review/presentation/ReviewController.java +++ b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java @@ -7,11 +7,13 @@ import com.webeye.backend.review.presentation.swagger.ReviewSwagger; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import static com.webeye.backend.global.dto.response.type.SuccessCode.REVIEW_SUMMARY_SUCCESS; +@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/v1/review") @@ -23,6 +25,13 @@ public class ReviewController implements ReviewSwagger { @PostMapping("/summary") @ResponseStatus(HttpStatus.OK) public SuccessResponse summarizeReview(@RequestBody @Valid ReviewSummaryRequest request) { + log.info("[리뷰 요약 요청] productId={}, totalCount={}, ratings={}, reviews={}", + request.productId(), + request.reviewRating().totalCount(), + request.reviewRating().ratings(), + request.reviews() + ); + return SuccessResponse.of(REVIEW_SUMMARY_SUCCESS, reviewService.summarizeReview(request)); } } From 41b935f978a5b7e4783eef968adafc29bfd4d1c5 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 00:19:14 +0900 Subject: [PATCH 213/309] =?UTF-8?q?#68=20feat:=20map=20->=20list=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 16 +++++++++++++++- .../review/dto/request/ReviewSummaryRequest.java | 11 +++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index db4b75f..e959e4d 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -12,6 +12,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + @Service @RequiredArgsConstructor public class ReviewService { @@ -28,10 +32,11 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { if (existingReview != null) { return ReviewMapper.toResponse(existingReview, request.reviewRating().totalCount()); } + Map ratingMap = convertToRatingMap(request.reviewRating().ratings()); ReviewSummaryResponse response = clovaXClientService.summarizeReviewText( request.reviews(), - request.reviewRating().ratings(), + ratingMap, request.reviewRating().totalCount() ); Review review = ReviewMapper.toEntity(response, product); @@ -49,4 +54,13 @@ public Product findOrCreateProduct(String productId) { .id(productId) .build())); } + + private Map convertToRatingMap(List ratingsList) { + List keys = List.of("최고", "좋음", "보통", "별로", "나쁨"); + Map ratingMap = new LinkedHashMap<>(); + for (int i = 0; i < keys.size(); i++) { + ratingMap.put(keys.get(i), i < ratingsList.size() ? ratingsList.get(i) : 0); + } + return ratingMap; + } } diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 54c6dd2..ec49202 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; +import java.util.List; import java.util.Map; @Builder @@ -35,14 +36,8 @@ public record ReviewRating( int totalCount, @Schema(description = "별점 등급별 수", example = """ - { - "최고": 83, - "좋음": 11, - "보통": 4, - "별로": 1, - "나쁨": 1 - } + [83, 11, 4, 1, 1] """) - Map ratings + List ratings ){} } From 73de72dd8a6013c8d3a88fe6196df5d8e5b05c46 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 02:00:32 +0900 Subject: [PATCH 214/309] =?UTF-8?q?#56=20refactor:=20webConfig=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index ddb14d5..b7f2093 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -15,7 +15,8 @@ public void addCorsMappings(CorsRegistry registry) { "http://localhost:3000", "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm", "chrome-extension://jeppkpjgeheckphiogogbffdenhlkclh", - "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf" + "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf", + "chrome-extension://aphlbfidmnkcjfanjfjhbfldlpgndgac" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 70862e6f372f423934f1f19020852a9f778a64ae Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 02:48:03 +0900 Subject: [PATCH 215/309] =?UTF-8?q?#65=20checkout:=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=A4=91=EA=B0=84=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/mapper/CosmeticMapper.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java diff --git a/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java new file mode 100644 index 0000000..6a1086e --- /dev/null +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java @@ -0,0 +1,18 @@ +package com.webeye.backend.cosmetic.infrastructure.mapper; + +import com.webeye.backend.cosmetic.domain.Cosmetic; +import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.product.domain.Product; + +public class CosmeticMapper { + + public static Cosmetic toEntity(CosmeticResponse response, Product product) { + return Cosmetic.builder().build(); + } + + public static CosmeticResponse toResponse (Cosmetic cosmetic) { + return CosmeticResponse.builder() + + .build(); + } +} From 3f2ffbed632d6c09353aa8f5df795f8d0fe86f02 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 11:02:53 +0900 Subject: [PATCH 216/309] =?UTF-8?q?#72=20feat:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=82=B4=20=EB=B3=84=EC=A0=90=EB=A7=8C=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20=EB=82=B4=EC=9A=A9=EC=9D=80=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=84=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 11 +++++++ .../clovaX/ClovaXClientService.java | 26 ++------------- .../infrastructure/mapper/ReviewMapper.java | 29 +++++++++++++++-- .../infrastructure/util/ReviewCalculator.java | 32 +++++++++++++++++++ 4 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index e959e4d..428f91a 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -8,6 +8,7 @@ import com.webeye.backend.review.infrastructure.clovaX.ClovaXClientService; import com.webeye.backend.review.infrastructure.mapper.ReviewMapper; import com.webeye.backend.review.infrastructure.persistence.ReviewRepository; +import com.webeye.backend.review.infrastructure.util.ReviewCalculator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,6 +35,16 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { } Map ratingMap = convertToRatingMap(request.reviewRating().ratings()); + if (request.reviews() == null || request.reviews().isEmpty()) { + double average = ReviewCalculator.calculateAverageRating(ratingMap, request.reviewRating().totalCount()); + ReviewSummaryResponse nullResponse = ReviewMapper.toNullResponse(request.reviewRating().totalCount(), average); + + Review review = ReviewMapper.toEntity(nullResponse, product); + reviewRepository.save(review); + + return ReviewMapper.toResponse(review, request.reviewRating().totalCount()); + } + ReviewSummaryResponse response = clovaXClientService.summarizeReviewText( request.reviews(), ratingMap, diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index da2ee49..53ca9fa 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -1,5 +1,6 @@ package com.webeye.backend.review.infrastructure.clovaX; +import com.webeye.backend.review.infrastructure.util.ReviewCalculator; import com.webeye.backend.review.dto.response.ReviewSummaryResponse; import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXContent; import com.webeye.backend.review.infrastructure.clovaX.dto.request.ClovaXMessage; @@ -28,7 +29,7 @@ public class ClovaXClientService { private String requestId; public ReviewSummaryResponse summarizeReviewText(Map> reviewText, Map ratingMap, int totalCount) { - double averageRating = calculateAverageRating(ratingMap, totalCount); + double averageRating = ReviewCalculator.calculateAverageRating(ratingMap, totalCount); String inputText = convertReviewMapToText(reviewText); ClovaXRequest clovaXRequest = new ClovaXRequest(List.of( @@ -113,27 +114,4 @@ private String convertReviewMapToText(Map> reviewMa } return sb.toString(); } - - private double calculateAverageRating(Map ratings, int totalCount) { - Map ratingPoint = Map.of( - "최고", 5, - "좋음", 4, - "보통", 3, - "별로", 2, - "나쁨", 1 - ); - int totalScore = ratings.entrySet().stream() - .mapToInt(entry -> { - Integer rating = ratingPoint.get(entry.getKey()); - Integer score = entry.getValue(); - - return rating != null ? rating * score : 0; - }) - .sum(); - - log.info("[평균 별점 계산] ratings: {}", ratings); - log.info("[평균 별점 계산] totalCount: {}", totalCount); - - return totalCount == 0 ? 0.0 : Math.round((totalScore / (double) totalCount) * 100.0) / 100.0; - } } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java index 8773cb2..7279e1b 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java @@ -5,15 +5,28 @@ import com.webeye.backend.review.dto.response.ReviewSummaryResponse; import java.util.Arrays; +import java.util.List; public class ReviewMapper { public static Review toEntity(ReviewSummaryResponse response, Product product) { Review review = Review.builder() .averageRating(response.averageRating()) - .positiveSummary(String.join("||", response.positiveReviews())) - .negativeSummary(String.join("||", response.negativeReviews())) - .keywords(String.join(",", response.keywords())) + .positiveSummary( + response.positiveReviews() == null || response.positiveReviews().isEmpty() + ? "리뷰가 없습니다." + : String.join("||", response.positiveReviews()) + ) + .negativeSummary( + response.negativeReviews() == null || response.negativeReviews().isEmpty() + ? "리뷰가 없습니다." + : String.join("||", response.negativeReviews()) + ) + .keywords( + response.keywords() == null || response.keywords().isEmpty() + ? "" + : String.join(",", response.keywords()) + ) .build(); review.associateWithProduct(product); @@ -29,4 +42,14 @@ public static ReviewSummaryResponse toResponse(Review review, int totalCount) { Arrays.asList(review.getKeywords().split(",")) ); } + + public static ReviewSummaryResponse toNullResponse(int totalCount, double averageRating) { + return new ReviewSummaryResponse( + totalCount, + averageRating, + List.of("리뷰가 존재하지 않습니다."), + List.of("리뷰가 존재하지 않습니다."), + List.of("리뷰가 존재하지 않습니다.") + ); + } } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java b/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java new file mode 100644 index 0000000..c61b7f3 --- /dev/null +++ b/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java @@ -0,0 +1,32 @@ +package com.webeye.backend.review.infrastructure.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +@Slf4j +public class ReviewCalculator { + + public static double calculateAverageRating(Map ratings, int totalCount) { + Map ratingPoint = Map.of( + "최고", 5, + "좋음", 4, + "보통", 3, + "별로", 2, + "나쁨", 1 + ); + int totalScore = ratings.entrySet().stream() + .mapToInt(entry -> { + Integer rating = ratingPoint.get(entry.getKey()); + Integer score = entry.getValue(); + + return rating != null ? rating * score : 0; + }) + .sum(); + + log.info("[평균 별점 계산] ratings: {}", ratings); + log.info("[평균 별점 계산] totalCount: {}", totalCount); + + return totalCount == 0 ? 0.0 : Math.round((totalScore / (double) totalCount) * 100.0) / 100.0; + } +} From 2475766e0e311fac4bc11168cbfefe975cfcb54a Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 11:17:42 +0900 Subject: [PATCH 217/309] =?UTF-8?q?#72=20feat:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=82=BC=ED=95=AD=EC=97=B0=EC=82=B0?= =?UTF-8?q?=EC=9E=90=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/mapper/ReviewMapper.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java index 7279e1b..81f5872 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/mapper/ReviewMapper.java @@ -12,21 +12,9 @@ public class ReviewMapper { public static Review toEntity(ReviewSummaryResponse response, Product product) { Review review = Review.builder() .averageRating(response.averageRating()) - .positiveSummary( - response.positiveReviews() == null || response.positiveReviews().isEmpty() - ? "리뷰가 없습니다." - : String.join("||", response.positiveReviews()) - ) - .negativeSummary( - response.negativeReviews() == null || response.negativeReviews().isEmpty() - ? "리뷰가 없습니다." - : String.join("||", response.negativeReviews()) - ) - .keywords( - response.keywords() == null || response.keywords().isEmpty() - ? "" - : String.join(",", response.keywords()) - ) + .positiveSummary(String.join("||", response.positiveReviews())) + .negativeSummary(String.join("||", response.negativeReviews())) + .keywords(String.join(",", response.keywords())) .build(); review.associateWithProduct(product); From 362a79da384bd3070feb09535ad7e05a06f2d82d Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 11:24:24 +0900 Subject: [PATCH 218/309] =?UTF-8?q?#72=20refactor:=20=ED=94=84=EB=A1=AC?= =?UTF-8?q?=ED=94=84=ED=8A=B8=EC=99=80=20clova=20x=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clovaX/ClovaXClientService.java | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 53ca9fa..12db849 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -32,42 +32,48 @@ public ReviewSummaryResponse summarizeReviewText(Map Date: Sat, 24 May 2025 11:27:28 +0900 Subject: [PATCH 219/309] =?UTF-8?q?#72=20docs:=20=EC=A3=BC=EC=84=9D=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 --- .../com/webeye/backend/review/application/ReviewService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 428f91a..0d494b0 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -29,12 +29,14 @@ public class ReviewService { public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { Product product = findOrCreateProduct(request.productId()); + // DB에 존재할 경우, DB에서 반환 Review existingReview = product.getReview(); if (existingReview != null) { return ReviewMapper.toResponse(existingReview, request.reviewRating().totalCount()); } Map ratingMap = convertToRatingMap(request.reviewRating().ratings()); + // 리뷰 내 별점만 존재하고 리뷰 내용은 존재하지 않을 경우 if (request.reviews() == null || request.reviews().isEmpty()) { double average = ReviewCalculator.calculateAverageRating(ratingMap, request.reviewRating().totalCount()); ReviewSummaryResponse nullResponse = ReviewMapper.toNullResponse(request.reviewRating().totalCount(), average); From b5377c847c36ed5c0e606fa4302aba9dfd8b7a1d Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 11:31:07 +0900 Subject: [PATCH 220/309] =?UTF-8?q?#72=20refactor:=20map=20->=20list=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EB=A9=94=EC=84=9C=EB=93=9C=20reviewCalcul?= =?UTF-8?q?ator=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/review/application/ReviewService.java | 11 +---------- .../review/infrastructure/util/ReviewCalculator.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 0d494b0..03d4d66 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -34,7 +34,7 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { if (existingReview != null) { return ReviewMapper.toResponse(existingReview, request.reviewRating().totalCount()); } - Map ratingMap = convertToRatingMap(request.reviewRating().ratings()); + Map ratingMap = ReviewCalculator.convertToRatingMap(request.reviewRating().ratings()); // 리뷰 내 별점만 존재하고 리뷰 내용은 존재하지 않을 경우 if (request.reviews() == null || request.reviews().isEmpty()) { @@ -67,13 +67,4 @@ public Product findOrCreateProduct(String productId) { .id(productId) .build())); } - - private Map convertToRatingMap(List ratingsList) { - List keys = List.of("최고", "좋음", "보통", "별로", "나쁨"); - Map ratingMap = new LinkedHashMap<>(); - for (int i = 0; i < keys.size(); i++) { - ratingMap.put(keys.get(i), i < ratingsList.size() ? ratingsList.get(i) : 0); - } - return ratingMap; - } } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java b/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java index c61b7f3..6ea65da 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java @@ -2,6 +2,8 @@ import lombok.extern.slf4j.Slf4j; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; @Slf4j @@ -29,4 +31,13 @@ public static double calculateAverageRating(Map ratings, int to return totalCount == 0 ? 0.0 : Math.round((totalScore / (double) totalCount) * 100.0) / 100.0; } + + public static Map convertToRatingMap(List ratingsList) { + List keys = List.of("최고", "좋음", "보통", "별로", "나쁨"); + Map ratingMap = new LinkedHashMap<>(); + for (int i = 0; i < keys.size(); i++) { + ratingMap.put(keys.get(i), i < ratingsList.size() ? ratingsList.get(i) : 0); + } + return ratingMap; + } } From 79b376c025d19031c03d688dfa1e00e7d8ef9053 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 11:34:56 +0900 Subject: [PATCH 221/309] =?UTF-8?q?#72=20chore:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20import=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/review/application/ReviewService.java | 2 -- src/main/java/com/webeye/backend/review/domain/Review.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 03d4d66..61efce8 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -13,8 +13,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; @Service diff --git a/src/main/java/com/webeye/backend/review/domain/Review.java b/src/main/java/com/webeye/backend/review/domain/Review.java index c7e3e7a..55652b9 100644 --- a/src/main/java/com/webeye/backend/review/domain/Review.java +++ b/src/main/java/com/webeye/backend/review/domain/Review.java @@ -23,7 +23,7 @@ public class Review extends BaseEntity { @Column(nullable = false) private Double averageRating; - + . @Column(nullable = false) private String positiveSummary; From ac803c2e0d4454a44ec937621c0d6d4621f90a74 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 11:54:50 +0900 Subject: [PATCH 222/309] =?UTF-8?q?#72=20chore:=20.=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/review/domain/Review.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/review/domain/Review.java b/src/main/java/com/webeye/backend/review/domain/Review.java index 55652b9..c7e3e7a 100644 --- a/src/main/java/com/webeye/backend/review/domain/Review.java +++ b/src/main/java/com/webeye/backend/review/domain/Review.java @@ -23,7 +23,7 @@ public class Review extends BaseEntity { @Column(nullable = false) private Double averageRating; - . + @Column(nullable = false) private String positiveSummary; From cd91dc24c93ed6ddca8c253f22b02942870471a4 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 13:56:54 +0900 Subject: [PATCH 223/309] =?UTF-8?q?#65=20remove:=20cosmetic=20domain=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CosmeticServiceImpl.java | 5 ---- .../backend/cosmetic/domain/Cosmetic.java | 30 ------------------- .../cosmetic/domain/CosmeticIngredient.java | 22 +++++++------- .../backend/cosmetic/domain/Ingredient.java | 11 +++++++ .../infrastructure/mapper/CosmeticMapper.java | 13 +++----- .../persistence/CosmeticRepository.java | 7 ----- .../backend/product/domain/Product.java | 14 +++++---- 7 files changed, 36 insertions(+), 66 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/cosmetic/domain/Cosmetic.java delete mode 100644 src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticRepository.java diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java index 45a20ef..2cb5283 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java @@ -1,6 +1,5 @@ package com.webeye.backend.cosmetic.application; -import com.webeye.backend.cosmetic.domain.Cosmetic; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; @@ -25,10 +24,6 @@ public class CosmeticServiceImpl implements CosmeticService { public CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request) { Product product = findOrCreateProduct(request.productId()); - Cosmetic existingCosmetic = product.getCosmetic(); - if (existingCosmetic != null) { - return null; - } return openAiClient.explainCosmetic(imageUrlExtractor.extractImageUrlFromHtml(request.html())); } diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/Cosmetic.java b/src/main/java/com/webeye/backend/cosmetic/domain/Cosmetic.java deleted file mode 100644 index 23423d5..0000000 --- a/src/main/java/com/webeye/backend/cosmetic/domain/Cosmetic.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.webeye.backend.cosmetic.domain; - -import com.webeye.backend.global.domain.BaseEntity; -import com.webeye.backend.product.domain.Product; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Cosmetic extends BaseEntity { - - @Id - @Column(name = "cosmetic_id", nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_id", unique = true) - private Product product; - - @Builder - public Cosmetic(Long id, Product product) { - this.id = id; - this.product = product; - } -} diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java b/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java index 2fdf366..0f9ba49 100644 --- a/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java +++ b/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java @@ -18,22 +18,24 @@ public class CosmeticIngredient extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "cosmetic_id") - private Cosmetic cosmetic; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") private Product product; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ingredient_id") + private Ingredient ingredient; + @Builder - public CosmeticIngredient( - Long id, - Cosmetic cosmetic, - Product product - ) { + public CosmeticIngredient(Long id) { this.id = id; - this.cosmetic = cosmetic; + } + + public void associateWithProduct(Product product) { this.product = product; } + + public void associateWithIngredient(Ingredient ingredient) { + this.ingredient = ingredient; + } } diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java b/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java index e211a19..91d2a26 100644 --- a/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java +++ b/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java @@ -8,6 +8,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -22,6 +25,9 @@ public class Ingredient extends BaseEntity { @Column(nullable = false, unique = true) private IngredientType ingredientType; + @OneToMany(mappedBy = "ingredient", cascade = CascadeType.ALL, orphanRemoval = true) + private List cosmeticIngredients = new ArrayList<>(); + @Builder public Ingredient( Long id, @@ -30,4 +36,9 @@ public Ingredient( this.id = id; this.ingredientType = ingredientType; } + + public void addCosmetic(CosmeticIngredient cosmeticIngredient) { + this.cosmeticIngredients.add(cosmeticIngredient); + cosmeticIngredient.associateWithIngredient(this); + } } diff --git a/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java index 6a1086e..f0fb041 100644 --- a/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java @@ -1,18 +1,13 @@ package com.webeye.backend.cosmetic.infrastructure.mapper; -import com.webeye.backend.cosmetic.domain.Cosmetic; -import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.cosmetic.domain.CosmeticIngredient; +import com.webeye.backend.cosmetic.domain.Ingredient; import com.webeye.backend.product.domain.Product; public class CosmeticMapper { - public static Cosmetic toEntity(CosmeticResponse response, Product product) { - return Cosmetic.builder().build(); - } - - public static CosmeticResponse toResponse (Cosmetic cosmetic) { - return CosmeticResponse.builder() - + public static CosmeticIngredient toEntity(Product product, Ingredient ingredient) { + return CosmeticIngredient.builder() .build(); } } diff --git a/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticRepository.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticRepository.java deleted file mode 100644 index 2401252..0000000 --- a/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/CosmeticRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.webeye.backend.cosmetic.infrastructure.persistence; - -import com.webeye.backend.cosmetic.domain.Cosmetic; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CosmeticRepository extends JpaRepository { -} diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 66a2c65..7b5658f 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -1,6 +1,6 @@ package com.webeye.backend.product.domain; -import com.webeye.backend.cosmetic.domain.Cosmetic; +import com.webeye.backend.cosmetic.domain.CosmeticIngredient; import com.webeye.backend.global.domain.BaseEntity; import com.webeye.backend.product.domain.type.ProductType; import com.webeye.backend.review.domain.Review; @@ -25,8 +25,8 @@ public class Product extends BaseEntity { @OneToOne(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private Review review; - @OneToOne(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - private Cosmetic cosmetic; + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + private List cosmeticIngredients = new ArrayList<>(); @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) private List allergies = new ArrayList<>(); @@ -38,10 +38,9 @@ public class Product extends BaseEntity { private List healthFoods = new ArrayList<>(); @Builder - public Product(String id, ProductType productType, Cosmetic cosmetic) { + public Product(String id, ProductType productType) { this.id = id; this.productType = productType; - this.cosmetic = cosmetic; } public void associateWithReview(Review review) { @@ -51,6 +50,11 @@ public void associateWithReview(Review review) { } } + public void addCosmeticIngredient(CosmeticIngredient cosmeticIngredient) { + this.cosmeticIngredients.add(cosmeticIngredient); + cosmeticIngredient.associateWithProduct(this); + } + public void addNutrient(ProductNutrient nutrient) { this.nutrients.add(nutrient); nutrient.associateWithProduct(this); From 2fa84518cf038a47079f0572faf7222e49b696b9 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 13:59:17 +0900 Subject: [PATCH 224/309] #65 rename: cosmeticMapper -> cosmeticIngredientMapper --- .../{CosmeticMapper.java => CosmeticIngredientMapper.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/{CosmeticMapper.java => CosmeticIngredientMapper.java} (90%) diff --git a/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java similarity index 90% rename from src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java rename to src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java index f0fb041..b58e7eb 100644 --- a/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticMapper.java +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java @@ -4,7 +4,7 @@ import com.webeye.backend.cosmetic.domain.Ingredient; import com.webeye.backend.product.domain.Product; -public class CosmeticMapper { +public class CosmeticIngredientMapper { public static CosmeticIngredient toEntity(Product product, Ingredient ingredient) { return CosmeticIngredient.builder() From bfd5e57fa6517aada9157b866a6884e635fc6e1e Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 14:22:45 +0900 Subject: [PATCH 225/309] =?UTF-8?q?#65=20fix:=20=EC=97=B0=EA=B4=80?= =?UTF-8?q?=EA=B4=80=EA=B3=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cosmetic/application/CosmeticServiceImpl.java | 2 +- .../backend/cosmetic/domain/CosmeticIngredient.java | 12 +++++------- .../webeye/backend/cosmetic/domain/Ingredient.java | 5 ----- .../mapper/CosmeticIngredientMapper.java | 2 ++ .../com/webeye/backend/product/domain/Product.java | 5 ----- .../product/persistent/ProductRepository.java | 4 ++-- 6 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java index 2cb5283..9d9d8d9 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java @@ -29,7 +29,7 @@ public CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request) { @Transactional public Product findOrCreateProduct(String productId) { - return productRepository.findByIdWithCosmetic(productId) + return productRepository.findByIdWithCosmeticIngredients(productId) .orElseGet(() -> productRepository.save( Product.builder() .id(productId) diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java b/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java index 0f9ba49..0f9b99d 100644 --- a/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java +++ b/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java @@ -27,15 +27,13 @@ public class CosmeticIngredient extends BaseEntity { private Ingredient ingredient; @Builder - public CosmeticIngredient(Long id) { + public CosmeticIngredient( + Long id, + Product product, + Ingredient ingredient + ) { this.id = id; - } - - public void associateWithProduct(Product product) { this.product = product; - } - - public void associateWithIngredient(Ingredient ingredient) { this.ingredient = ingredient; } } diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java b/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java index 91d2a26..ef137a9 100644 --- a/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java +++ b/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java @@ -36,9 +36,4 @@ public Ingredient( this.id = id; this.ingredientType = ingredientType; } - - public void addCosmetic(CosmeticIngredient cosmeticIngredient) { - this.cosmeticIngredients.add(cosmeticIngredient); - cosmeticIngredient.associateWithIngredient(this); - } } diff --git a/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java index b58e7eb..d905ce1 100644 --- a/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java @@ -8,6 +8,8 @@ public class CosmeticIngredientMapper { public static CosmeticIngredient toEntity(Product product, Ingredient ingredient) { return CosmeticIngredient.builder() + .product(product) + .ingredient(ingredient) .build(); } } diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 7b5658f..5b927c7 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -50,11 +50,6 @@ public void associateWithReview(Review review) { } } - public void addCosmeticIngredient(CosmeticIngredient cosmeticIngredient) { - this.cosmeticIngredients.add(cosmeticIngredient); - cosmeticIngredient.associateWithProduct(this); - } - public void addNutrient(ProductNutrient nutrient) { this.nutrients.add(nutrient); nutrient.associateWithProduct(this); diff --git a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java index 5d147e5..0fb1c07 100644 --- a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java +++ b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java @@ -15,6 +15,6 @@ public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p LEFT JOIN FETCH p.review WHERE p.id = :id") Optional findByIdWithReview(@Param("id") String id); - @Query("SELECT p FROM Product p LEFT JOIN p.cosmetic WHERE p.id = :id") - Optional findByIdWithCosmetic(@Param("id") String id); + @Query("SELECT p FROM Product p LEFT JOIN p.cosmeticIngredients WHERE p.id = :id") + Optional findByIdWithCosmeticIngredients(@Param("id") String id); } From 57ceb9184dde8f7a763fd1fea80efa4f10dff159 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 14:42:49 +0900 Subject: [PATCH 226/309] =?UTF-8?q?#74=20refactor:=20webConfig=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index b7f2093..676d5d9 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -16,7 +16,9 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm", "chrome-extension://jeppkpjgeheckphiogogbffdenhlkclh", "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf", - "chrome-extension://aphlbfidmnkcjfanjfjhbfldlpgndgac" + "chrome-extension://aphlbfidmnkcjfanjfjhbfldlpgndgac", + "chrome-extension://ghplnadekipfddbmbpcpkophjobfpkkj", + "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 7ffd0371bd47571626b7af63982c8086b145083c Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 14:45:48 +0900 Subject: [PATCH 227/309] =?UTF-8?q?#74=20refactor:=20=E3=85=85=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 676d5d9..4de9cb6 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -18,7 +18,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf", "chrome-extension://aphlbfidmnkcjfanjfjhbfldlpgndgac", "chrome-extension://ghplnadekipfddbmbpcpkophjobfpkkj", - "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh" + "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh", + "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 4b8539edd6cc738c6f9baced614e1e870ed60771 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 15:04:52 +0900 Subject: [PATCH 228/309] =?UTF-8?q?#72=20refactor:=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/util/ReviewCalculator.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java b/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java index 6ea65da..07cb052 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/util/ReviewCalculator.java @@ -9,19 +9,21 @@ @Slf4j public class ReviewCalculator { + public static final List RATING_LABELS = List.of("최고", "좋음", "보통", "별로", "나쁨"); + + public static final Map RATING_POINTS = Map.of( + "최고", 5, + "좋음", 4, + "보통", 3, + "별로", 2, + "나쁨", 1 + ); + public static double calculateAverageRating(Map ratings, int totalCount) { - Map ratingPoint = Map.of( - "최고", 5, - "좋음", 4, - "보통", 3, - "별로", 2, - "나쁨", 1 - ); int totalScore = ratings.entrySet().stream() .mapToInt(entry -> { - Integer rating = ratingPoint.get(entry.getKey()); + Integer rating = RATING_POINTS.get(entry.getKey()); Integer score = entry.getValue(); - return rating != null ? rating * score : 0; }) .sum(); @@ -33,10 +35,9 @@ public static double calculateAverageRating(Map ratings, int to } public static Map convertToRatingMap(List ratingsList) { - List keys = List.of("최고", "좋음", "보통", "별로", "나쁨"); Map ratingMap = new LinkedHashMap<>(); - for (int i = 0; i < keys.size(); i++) { - ratingMap.put(keys.get(i), i < ratingsList.size() ? ratingsList.get(i) : 0); + for (int i = 0; i < RATING_LABELS.size(); i++) { + ratingMap.put(RATING_LABELS.get(i), i < ratingsList.size() ? ratingsList.get(i) : 0); } return ratingMap; } From 52b0f704715ba0061dc11ebcfa83869bed3e2e2f Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 16:55:30 +0900 Subject: [PATCH 229/309] =?UTF-8?q?#76=20refactor:=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/webeye/backend/global/config/WebConfig.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 4de9cb6..b7f2093 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -16,10 +16,7 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm", "chrome-extension://jeppkpjgeheckphiogogbffdenhlkclh", "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf", - "chrome-extension://aphlbfidmnkcjfanjfjhbfldlpgndgac", - "chrome-extension://ghplnadekipfddbmbpcpkophjobfpkkj", - "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh", - "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb" + "chrome-extension://aphlbfidmnkcjfanjfjhbfldlpgndgac" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From a984c6f53f44df1e1a8ed8b6ea8389bc804d5748 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 24 May 2025 17:00:27 +0900 Subject: [PATCH 230/309] =?UTF-8?q?#71=20feat:=20=EC=8B=9D=ED=92=88=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20API=EC=97=90=20=EC=98=81=EC=96=91=EC=84=B1?= =?UTF-8?q?=EB=B6=84=20=EB=8F=84=EC=B6=9C=20=EA=B8=B0=EC=A4=80=20=ED=95=A8?= =?UTF-8?q?=EB=9F=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nutrition/dto/response/NutrientResponse.java | 16 ++++++++++++++++ .../product/dto/response/ProductResponse.java | 5 ++--- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/webeye/backend/nutrition/dto/response/NutrientResponse.java diff --git a/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientResponse.java b/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientResponse.java new file mode 100644 index 0000000..df57465 --- /dev/null +++ b/src/main/java/com/webeye/backend/nutrition/dto/response/NutrientResponse.java @@ -0,0 +1,16 @@ +package com.webeye.backend.nutrition.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.util.List; + +@Builder +public record NutrientResponse( + @Schema(description = "영양성분 도출 기준 함량(단위: g)") + Integer nutrientReferenceAmount, + + @Schema(description = "영양성분 권장량을 넘는 성분") + List overRecommendationNutrients +) { +} diff --git a/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java index ab55960..bf8596a 100644 --- a/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java +++ b/src/main/java/com/webeye/backend/product/dto/response/ProductResponse.java @@ -1,7 +1,7 @@ package com.webeye.backend.product.dto.response; import com.webeye.backend.allergy.type.AllergyType; -import com.webeye.backend.nutrition.dto.response.NutrientRecommendationResponse; +import com.webeye.backend.nutrition.dto.response.NutrientResponse; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; @@ -13,7 +13,6 @@ public record ProductResponse ( @Schema(description = "포함된 알레르기 유발 성분") List allergyTypes, - @Schema(description = "영양성분 권장량을 넘는 성분") - List overRecommendationNutrients + NutrientResponse nutrientResponse ) { } From 71649484f703119f7225fef2df16814ffb30760d Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 24 May 2025 17:01:11 +0900 Subject: [PATCH 231/309] =?UTF-8?q?#71=20feat:=20OpenAI=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=98=81=EC=96=91=EC=84=B1=EB=B6=84=20=EB=8F=84=EC=B6=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=ED=95=A8=EB=9F=89=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 4 ++++ .../backend/nutrition/dto/response/NutritionAiResponse.java | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 72f1c6f..6f35e18 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -92,6 +92,9 @@ public NutritionAiResponse explainNutrition(List urls) { String user = """ If the attached images contain 'nutrition information', please provide the amount of each nutrient in the format I sent. If the nutritional information is not included, set the isNutrientIncluded field to false; if it is included, set it to true. + You are given a nutrition label image. Extract the number of grams that the nutritional values are based on. + This is typically written as "per 100g", "per 1 serving (XXg)", "100 g당" (in Korean), or similar. + Return only the numeric gram value in a field named nutrientReferenceAmount. """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); @@ -136,6 +139,7 @@ public CosmeticResponse explainCosmetic(List urls) { } Return true only if the exact full Korean ingredient name appears continuously and separately; otherwise, return false. Ignore partial, similar, or incomplete matches. + """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); diff --git a/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java index 003d889..476aebb 100644 --- a/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java +++ b/src/main/java/com/webeye/backend/nutrition/dto/response/NutritionAiResponse.java @@ -8,6 +8,9 @@ public record NutritionAiResponse( @Schema(description = "영양성분 정보 포함 여부") Boolean isNutrientIncluded, + @Schema(description = "영양성분 도출 기준 함량") + Integer nutrientReferenceAmount, + @Schema(description = "나트륨 (mg)", example = "120.5") Double sodium, @Schema(description = "탄수화물 (g)", example = "25.0") From aa9db938e3618bb6ebd5ee2afea77222782680cb Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 24 May 2025 17:02:00 +0900 Subject: [PATCH 232/309] =?UTF-8?q?#71=20feat:=20Product=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EC=97=90=20nutrientReferenceAmount=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 --- .../java/com/webeye/backend/product/domain/Product.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 1e86ef4..c1496bd 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -16,6 +16,8 @@ public class Product extends BaseEntity { @Column(name = "product_id", nullable = false) private String id; // 쿠팡에서 products 뒤에 오는 숫자 + private Integer nutrientReferenceAmount; + @OneToOne(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private Review review; @@ -29,8 +31,9 @@ public class Product extends BaseEntity { private List healthFoods = new ArrayList<>(); @Builder - public Product(String id) { + public Product(String id, Integer nutrientReferenceAmount) { this.id = id; + this.nutrientReferenceAmount = nutrientReferenceAmount; } public void associateWithReview(Review review) { @@ -54,4 +57,8 @@ public void addHealthFood(ProductHealthfood healthFood) { this.healthFoods.add(healthFood); healthFood.associateWithProduct(this); } + + public void setNutrientReferenceAmount(Integer nutrientReferenceAmount) { + this.nutrientReferenceAmount = nutrientReferenceAmount; + } } \ No newline at end of file From 44bbff1dce913473751d5f462b7078f599c0602e Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 24 May 2025 17:03:04 +0900 Subject: [PATCH 233/309] =?UTF-8?q?#71=20feat:=20nutrientReferenceAmount?= =?UTF-8?q?=20=EB=A5=BC=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nutrition/application/NutritionService.java | 4 +--- .../product/application/ProductService.java | 15 +++++++++------ .../application/RawMaterialService.java | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index 74de755..ab73618 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -11,7 +11,6 @@ import com.webeye.backend.nutrition.persistent.NutrientRepository; import com.webeye.backend.product.domain.Product; import com.webeye.backend.product.domain.ProductNutrient; -import com.webeye.backend.product.persistent.ProductRepository; import com.webeye.backend.rawmaterial.application.RawMaterialService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -29,7 +28,6 @@ public class NutritionService { private final RawMaterialService rawMaterialService; private final NutrientRepository nutrientRepository; - private final ProductRepository productRepository; public Nutrient findByType(NutrientType type) { return nutrientRepository.findByType(type) @@ -40,6 +38,7 @@ public Nutrient findByType(NutrientType type) { public void saveProductNutrition(Product product, FoodProductAnalysisRequest request) { NutritionAiResponse response = openAiClient.explainNutrition(imageUrlExtractor.extractImageUrlFromHtml(request.html())); if (Boolean.TRUE.equals(response.isNutrientIncluded())) { + product.setNutrientReferenceAmount(response.nutrientReferenceAmount()); Map nutrientMap = extractNutrientMap(response); nutrientMap.forEach((type, amount) -> { @@ -53,7 +52,6 @@ public void saveProductNutrition(Product product, FoodProductAnalysisRequest req .build() ); }); - productRepository.save(product); return; } rawMaterialService.saveRawMaterialNutrition(product, request); diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index c1952f5..a804548 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -3,13 +3,13 @@ import com.webeye.backend.allergy.application.AllergyService; import com.webeye.backend.allergy.type.AllergyType; import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; +import com.webeye.backend.nutrition.dto.response.NutrientResponse; import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.nutrition.application.NutrientRecommendationService; import com.webeye.backend.nutrition.dto.request.NutrientRecommendationRequest; -import com.webeye.backend.nutrition.dto.response.NutrientRecommendationResponse; import com.webeye.backend.product.domain.ProductAllergy; import com.webeye.backend.product.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; @@ -46,7 +46,7 @@ public ProductResponse analyzeFoodProduct(FoodProductAnalysisRequest request) { return ProductResponse.builder() .allergyTypes(getAllergyResponse(product, request.allergies())) - .overRecommendationNutrients(getNutrientRecommendationResponse(request, product)) + .nutrientResponse(getNutrientRecommendationResponse(request, product)) .build(); } Product product = Product.builder() @@ -59,7 +59,7 @@ public ProductResponse analyzeFoodProduct(FoodProductAnalysisRequest request) { return ProductResponse.builder() .allergyTypes(getAllergyResponse(product, request.allergies())) - .overRecommendationNutrients(getNutrientRecommendationResponse(request, product)) + .nutrientResponse(getNutrientRecommendationResponse(request, product)) .build(); } @@ -71,10 +71,13 @@ private List getAllergyResponse(Product product, List .toList(); } - private List getNutrientRecommendationResponse( + private NutrientResponse getNutrientRecommendationResponse( FoodProductAnalysisRequest request, Product product) { - return nutrientRecommendationService.analyzeNutrientSufficiency(NutrientRecommendationRequest - .builder().birthYear(request.birthYear()).gender(request.gender()).product(product).build()); + return NutrientResponse.builder() + .nutrientReferenceAmount(product.getNutrientReferenceAmount()) + .overRecommendationNutrients(nutrientRecommendationService.analyzeNutrientSufficiency(NutrientRecommendationRequest + .builder().birthYear(request.birthYear()).gender(request.gender()).product(product).build())) + .build(); } public DetailExplanationResponse analyzeProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java index 98313a1..3ab7cbc 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java @@ -32,6 +32,7 @@ public class RawMaterialService { @Transactional public void saveRawMaterialNutrition(Product product, FoodProductAnalysisRequest request) { + product.setNutrientReferenceAmount(100); String foodName = openAiClient.explainRawMaterial(request).name(); Optional rawMaterialOpt = rawMaterialRepository.findFirstByNameContaining(foodName); rawMaterialOpt.ifPresentOrElse(rawMaterial -> { @@ -52,7 +53,7 @@ public void saveRawMaterialNutrition(Product product, FoodProductAnalysisRequest productRepository.save(product); }, () -> log.warn("일치하는 원재료 데이터가 존재하지 않습니다.")); } - + private Map convertToNutrientMap(RawMaterial rawMaterial) { Map map = new EnumMap<>(NutrientType.class); map.put(NutrientType.SODIUM, rawMaterial.getSodium()); From cb578fc46e91e0dfb9cb0683d8840f3cabcb644a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 24 May 2025 17:30:37 +0900 Subject: [PATCH 234/309] #71 refactor: getNutrientRecommendationResponse -> getNutrientResponse --- .../webeye/backend/product/application/ProductService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index a804548..5538e31 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -46,7 +46,7 @@ public ProductResponse analyzeFoodProduct(FoodProductAnalysisRequest request) { return ProductResponse.builder() .allergyTypes(getAllergyResponse(product, request.allergies())) - .nutrientResponse(getNutrientRecommendationResponse(request, product)) + .nutrientResponse(getNutrientResponse(request, product)) .build(); } Product product = Product.builder() @@ -59,7 +59,7 @@ public ProductResponse analyzeFoodProduct(FoodProductAnalysisRequest request) { return ProductResponse.builder() .allergyTypes(getAllergyResponse(product, request.allergies())) - .nutrientResponse(getNutrientRecommendationResponse(request, product)) + .nutrientResponse(getNutrientResponse(request, product)) .build(); } @@ -71,7 +71,7 @@ private List getAllergyResponse(Product product, List .toList(); } - private NutrientResponse getNutrientRecommendationResponse( + private NutrientResponse getNutrientResponse( FoodProductAnalysisRequest request, Product product) { return NutrientResponse.builder() .nutrientReferenceAmount(product.getNutrientReferenceAmount()) From 022b21cb31ccc8ee00dfe18b5831fce8ed2c6350 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 24 May 2025 17:31:06 +0900 Subject: [PATCH 235/309] =?UTF-8?q?#71=20fix:=20product=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=8B=A4=EC=8B=9C=20=EC=A0=80=EC=9E=A5=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/rawmaterial/application/RawMaterialService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java index 3ab7cbc..be3c2ac 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java @@ -49,8 +49,6 @@ public void saveRawMaterialNutrition(Product product, FoodProductAnalysisRequest .build() ); }); - - productRepository.save(product); }, () -> log.warn("일치하는 원재료 데이터가 존재하지 않습니다.")); } From 02601848bbb6a7377dfa9ba41ef911ac18b8e573 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 17:31:39 +0900 Subject: [PATCH 236/309] =?UTF-8?q?#79=20refactor:=20webConfig=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index b7f2093..6c1e00a 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -16,7 +16,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm", "chrome-extension://jeppkpjgeheckphiogogbffdenhlkclh", "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf", - "chrome-extension://aphlbfidmnkcjfanjfjhbfldlpgndgac" + "chrome-extension://aphlbfidmnkcjfanjfjhbfldlpgndgac", + "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 488cec31f46966686f3cebb549e02c556a958ce4 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 24 May 2025 17:47:05 +0900 Subject: [PATCH 237/309] =?UTF-8?q?#71=20refactor:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=20=EB=AC=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/rawmaterial/application/RawMaterialService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java index be3c2ac..e2273bb 100644 --- a/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java +++ b/src/main/java/com/webeye/backend/rawmaterial/application/RawMaterialService.java @@ -7,7 +7,6 @@ import com.webeye.backend.product.domain.Product; import com.webeye.backend.product.domain.ProductNutrient; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; -import com.webeye.backend.product.persistent.ProductRepository; import com.webeye.backend.rawmaterial.domain.RawMaterial; import com.webeye.backend.rawmaterial.persistent.RawMaterialRepository; import lombok.RequiredArgsConstructor; @@ -28,7 +27,6 @@ public class RawMaterialService { private final RawMaterialRepository rawMaterialRepository; private final NutrientRepository nutrientRepository; - private final ProductRepository productRepository; @Transactional public void saveRawMaterialNutrition(Product product, FoodProductAnalysisRequest request) { From a452e9db15f1b225c7c1e6f394f1723e819cd8d6 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 24 May 2025 17:54:49 +0900 Subject: [PATCH 238/309] =?UTF-8?q?#79=20refactor:=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/webeye/backend/global/config/WebConfig.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 6c1e00a..0799570 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -16,8 +16,11 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm", "chrome-extension://jeppkpjgeheckphiogogbffdenhlkclh", "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf", - "chrome-extension://aphlbfidmnkcjfanjfjhbfldlpgndgac", - "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj" + "chrome-extension://adkanhcmkdbilgdibndapnlhkcnfoglg", + "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj", + "chrome-extension://ghplnadekipfddbmbpcpkophjobfpkkj", + "chrome-extension://alnggnpkcolckmkepmkhkdcajipincml", + "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From a4dd205a744dd0007af528fb6dfb361616a1faef Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 25 May 2025 15:15:26 +0900 Subject: [PATCH 239/309] =?UTF-8?q?#65=20feat:=20cosmetic=20db=EB=82=B4=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CosmeticServiceImpl.java | 60 ++++++++++++++++++- .../cosmetic/domain/CosmeticIngredient.java | 2 - .../mapper/CosmeticIngredientMapper.java | 35 +++++++++++ .../backend/global/error/ErrorCode.java | 3 + 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java index 9d9d8d9..4ae87ca 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java @@ -1,6 +1,14 @@ package com.webeye.backend.cosmetic.application; +import com.webeye.backend.cosmetic.domain.CosmeticIngredient; +import com.webeye.backend.cosmetic.domain.Ingredient; +import com.webeye.backend.cosmetic.domain.type.IngredientType; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.cosmetic.infrastructure.mapper.CosmeticIngredientMapper; +import com.webeye.backend.cosmetic.infrastructure.persistence.CosmeticIngredientRepository; +import com.webeye.backend.cosmetic.infrastructure.persistence.IngredientRepository; +import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.global.error.ErrorCode; import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.product.domain.Product; @@ -11,6 +19,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Map; + @Service @RequiredArgsConstructor public class CosmeticServiceImpl implements CosmeticService { @@ -18,13 +28,36 @@ public class CosmeticServiceImpl implements CosmeticService { private final OpenAiClient openAiClient; private final ImageUrlExtractor imageUrlExtractor; private final ProductRepository productRepository; + private final IngredientRepository ingredientRepository; + private final CosmeticIngredientRepository cosmeticIngredientRepository; @Override @Transactional public CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request) { Product product = findOrCreateProduct(request.productId()); - return openAiClient.explainCosmetic(imageUrlExtractor.extractImageUrlFromHtml(request.html())); + // DB에 있을 경우, DB에서 조회 후 호출 + if (product.getCosmeticIngredients() != null && !product.getCosmeticIngredients().isEmpty()) { + return CosmeticIngredientMapper.toResponse(product.getCosmeticIngredients()); + } + + CosmeticResponse response = openAiClient.explainCosmetic( + imageUrlExtractor.extractImageUrlFromHtml(request.html()) + ); + + Map resultMap = convertToEnumMap(response); + + resultMap.entrySet().stream() + .filter(Map.Entry::getValue) + .forEach(entry -> { + Ingredient ingredient = ingredientRepository.findByIngredientType(entry.getKey()) + .orElseThrow(() -> new BusinessException(ErrorCode.COSMETIC_INGREDIENT_NOT_FOUND)); + + CosmeticIngredient cosmeticIngredient = CosmeticIngredientMapper.toEntity(product, ingredient); + cosmeticIngredientRepository.save(cosmeticIngredient); + }); + + return response; } @Transactional @@ -36,4 +69,29 @@ public Product findOrCreateProduct(String productId) { .productType(ProductType.COSMETIC) .build())); } + + private Map convertToEnumMap(CosmeticResponse response) { + return Map.ofEntries( + Map.entry(IngredientType.AVOBENZONE, response.avobenzone()), + Map.entry(IngredientType.ISOPROPYL_ALCOHOL, response.isopropylAlcohol()), + Map.entry(IngredientType.SODIUM_LAURYL_SULFATE, response.sodiumLaurylSulfate()), + Map.entry(IngredientType.TRIETHANOLAMINE, response.triethanolamine()), + Map.entry(IngredientType.POLYETHYLENE_GLYCOL, response.polyethyleneGlycol()), + Map.entry(IngredientType.SYNTHETIC_COLORANT, response.syntheticColorant()), + Map.entry(IngredientType.ISOPROPYL_METHYLPHENOL, response.isopropylMethylphenol()), + Map.entry(IngredientType.SORBIC_ACID, response.sorbicAcid()), + Map.entry(IngredientType.HORMONE, response.hormone()), + Map.entry(IngredientType.DIBUTYL_HYDROXYTOLUENE, response.dibutylHydroxyToluene()), + Map.entry(IngredientType.PARABENS, response.parabens()), + Map.entry(IngredientType.TRICLOSAN, response.triclosan()), + Map.entry(IngredientType.BUTYLATED_HYDROXYANISOLE, response.butylatedHydroxyanisole()), + Map.entry(IngredientType.OXYBENZONE, response.oxybenzone()), + Map.entry(IngredientType.IMIDAZOLIDINYL_UREA, response.imidazolidinylUrea()), + Map.entry(IngredientType.MINERAL_OIL, response.mineralOil()), + Map.entry(IngredientType.THYMOL, response.thymol()), + Map.entry(IngredientType.TRIISOPROPANOLAMINE, response.triisopropanolamine()), + Map.entry(IngredientType.SYNTHETIC_FRAGRANCE, response.syntheticFragrance()), + Map.entry(IngredientType.PHENOXYETHANOL, response.phenoxyethanol()) + ); + } } diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java b/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java index 0f9b99d..b3598b5 100644 --- a/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java +++ b/src/main/java/com/webeye/backend/cosmetic/domain/CosmeticIngredient.java @@ -28,11 +28,9 @@ public class CosmeticIngredient extends BaseEntity { @Builder public CosmeticIngredient( - Long id, Product product, Ingredient ingredient ) { - this.id = id; this.product = product; this.ingredient = ingredient; } diff --git a/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java index d905ce1..f3a7048 100644 --- a/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/mapper/CosmeticIngredientMapper.java @@ -2,8 +2,14 @@ import com.webeye.backend.cosmetic.domain.CosmeticIngredient; import com.webeye.backend.cosmetic.domain.Ingredient; +import com.webeye.backend.cosmetic.domain.type.IngredientType; +import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.product.domain.Product; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + public class CosmeticIngredientMapper { public static CosmeticIngredient toEntity(Product product, Ingredient ingredient) { @@ -12,4 +18,33 @@ public static CosmeticIngredient toEntity(Product product, Ingredient ingredient .ingredient(ingredient) .build(); } + + public static CosmeticResponse toResponse(List ingredients) { + Set present = ingredients.stream() + .map(ci -> ci.getIngredient().getIngredientType()) + .collect(Collectors.toSet()); + + return CosmeticResponse.builder() + .avobenzone(present.contains(IngredientType.AVOBENZONE)) + .isopropylAlcohol(present.contains(IngredientType.ISOPROPYL_ALCOHOL)) + .sodiumLaurylSulfate(present.contains(IngredientType.SODIUM_LAURYL_SULFATE)) + .triethanolamine(present.contains(IngredientType.TRIETHANOLAMINE)) + .polyethyleneGlycol(present.contains(IngredientType.POLYETHYLENE_GLYCOL)) + .syntheticColorant(present.contains(IngredientType.SYNTHETIC_COLORANT)) + .isopropylMethylphenol(present.contains(IngredientType.ISOPROPYL_METHYLPHENOL)) + .sorbicAcid(present.contains(IngredientType.SORBIC_ACID)) + .hormone(present.contains(IngredientType.HORMONE)) + .dibutylHydroxyToluene(present.contains(IngredientType.DIBUTYL_HYDROXYTOLUENE)) + .parabens(present.contains(IngredientType.PARABENS)) + .triclosan(present.contains(IngredientType.TRICLOSAN)) + .butylatedHydroxyanisole(present.contains(IngredientType.BUTYLATED_HYDROXYANISOLE)) + .oxybenzone(present.contains(IngredientType.OXYBENZONE)) + .imidazolidinylUrea(present.contains(IngredientType.IMIDAZOLIDINYL_UREA)) + .mineralOil(present.contains(IngredientType.MINERAL_OIL)) + .thymol(present.contains(IngredientType.THYMOL)) + .triisopropanolamine(present.contains(IngredientType.TRIISOPROPANOLAMINE)) + .syntheticFragrance(present.contains(IngredientType.SYNTHETIC_FRAGRANCE)) + .phenoxyethanol(present.contains(IngredientType.PHENOXYETHANOL)) + .build(); + } } diff --git a/src/main/java/com/webeye/backend/global/error/ErrorCode.java b/src/main/java/com/webeye/backend/global/error/ErrorCode.java index 25b9e1d..4a30a8e 100644 --- a/src/main/java/com/webeye/backend/global/error/ErrorCode.java +++ b/src/main/java/com/webeye/backend/global/error/ErrorCode.java @@ -26,6 +26,9 @@ public enum ErrorCode { // nutrient NUTRIENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 영양성분입니다."), + + // cosmetic + COSMETIC_INGREDIENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 화장품 성분입니다.") ; private final HttpStatus status; private final String errorMessage; From c5e96ff42e2a31fa19ecb1f46e632047b407eae1 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 25 May 2025 16:29:20 +0900 Subject: [PATCH 240/309] =?UTF-8?q?#65=20feat:=20prompt=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../imageanalysis/infrastructure/OpenAiClient.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 72f1c6f..da32fbb 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -106,10 +106,12 @@ public CosmeticResponse explainCosmetic(List urls) { """; String user = """ - Carefully examine the attached image. Focus only on Korean ingredient names. + Carefully examine the attached image. Focus only on **Korean ingredient names** that appear under the section labeled **"전성분"**. - Your goal is to check for the exact **presence** of the following Korean ingredient names: - (These names must appear exactly and completely in the image, including spacing and punctuation) + Your task is to check for the exact **presence** of the following Korean ingredient names — but **only within the "전성분" section**. + + These names must appear **exactly and completely**, including spacing and punctuation. + Ignore any matches that appear **outside of the "전성분" section**, or are **partial/similar**. Use this mapping: { @@ -136,6 +138,8 @@ public CosmeticResponse explainCosmetic(List urls) { } Return true only if the exact full Korean ingredient name appears continuously and separately; otherwise, return false. Ignore partial, similar, or incomplete matches. + + Note: "인공 향료 (Synthetic Fragrance, Parfum)" is considered the same as "향료" or "Fragrance" — treat all of them as matching "syntheticFragrance". """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); From ddab1e0b31f6ab4f0940cef7287bd42f5f03a17c Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 25 May 2025 16:35:48 +0900 Subject: [PATCH 241/309] =?UTF-8?q?#65=20feat:=20healthFood=20prompt=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index da32fbb..2b9e89a 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -158,6 +158,7 @@ public HealthFoodAiResponse explainHealthFood(List urls) { Look for the section that lists ingredients. This section is usually labeled with: - '원재료명' - '원재료 및 함량' + - '영양·기능정보' - or similar titles From this section, extract only the ingredient names. For example: From 7d9b0d2b8571ce3342191a59271dfd6b0aa6dba4 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 25 May 2025 17:27:50 +0900 Subject: [PATCH 242/309] =?UTF-8?q?#65=20feat:=20healthFood=20DB=EB=82=B4?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=ED=9B=84=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HealthFoodService.java | 32 +++++++++---------- .../mapper/HealthFoodMapper.java | 25 ++++++++++++++- .../product/persistent/ProductRepository.java | 3 ++ 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index e9db47e..abe96f7 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -15,6 +15,7 @@ import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.product.domain.Product; import com.webeye.backend.product.domain.ProductHealthfood; +import com.webeye.backend.product.domain.type.ProductType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.product.persistent.ProductHealthFoodRepository; import com.webeye.backend.product.persistent.ProductRepository; @@ -61,12 +62,12 @@ public HealthFoodResponse.I2710 callHealthFoodApi() { @Transactional public HealthFoodKeywordResponse analyzeAndSaveHealthFood(FoodProductAnalysisRequest request) { - Product product = productRepository.findById(request.productId()) - .orElseGet(() -> productRepository.save( - Product.builder() - .id(request.productId()) - .build() - )); + Product product = findOrCreateProduct(request.productId()); + + // DB에 있을 경우, DB에서 조회 후 호출 + if (product.getHealthFoods() != null && !product.getHealthFoods().isEmpty()) { + return HealthFoodMapper.toResponseFromProduct(product); + } HealthFoodAiResponse response = analyzeHealthFood(request); List dbItemNames = healthFoodRepository.findAllItemNames(); @@ -76,9 +77,7 @@ public HealthFoodKeywordResponse analyzeAndSaveHealthFood(FoodProductAnalysisReq saveProductHealthFood(product, healthFoods); - List types = mapHealthFoodTypes(healthFoods); - - return HealthFoodMapper.toResponse(types); + return HealthFoodMapper.toResponseFromHealthFoods(healthFoods); } public HealthFoodAiResponse analyzeHealthFood(FoodProductAnalysisRequest request) { @@ -94,13 +93,14 @@ public void saveProductHealthFood(Product product, List healthFoods) productHealthFoodRepository.saveAll(productHealthFoods); } - private List mapHealthFoodTypes(List healthFoods) { - return healthFoods.stream() - .flatMap(healthFood -> healthFood.getHealthFoodKeywords().stream()) - .map(HealthFoodKeyword::getKeyword) - .map(Keyword::getType) - .distinct() - .toList(); + @Transactional + public Product findOrCreateProduct(String productId) { + return productRepository.findByIdWithHealthFoods(productId) + .orElseGet(() -> productRepository.save( + Product.builder() + .id(productId) + .productType(ProductType.HEALTH_FOOD) + .build())); } private List matchItemNames(List aiItemNames, List dbItemNames) { diff --git a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java index b2967ed..eae2b86 100644 --- a/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java +++ b/src/main/java/com/webeye/backend/healthfood/infrastructure/mapper/HealthFoodMapper.java @@ -1,9 +1,13 @@ package com.webeye.backend.healthfood.infrastructure.mapper; import com.webeye.backend.healthfood.domain.HealthFood; +import com.webeye.backend.healthfood.domain.HealthFoodKeyword; +import com.webeye.backend.healthfood.domain.Keyword; import com.webeye.backend.healthfood.domain.type.HealthFoodType; import com.webeye.backend.healthfood.dto.HealthFoodKeywordResponse; import com.webeye.backend.healthfood.dto.HealthFoodResponse; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.domain.ProductHealthfood; import java.util.List; import java.util.stream.Collectors; @@ -38,9 +42,28 @@ public static HealthFoodResponse.I2710 toResponseList(List healthFoo return new HealthFoodResponse.I2710(totalCount, i2710List); } - public static HealthFoodKeywordResponse toResponse(List types) { + public static HealthFoodKeywordResponse toResponseFromProduct(Product product) { + List healthFoods = product.getHealthFoods().stream() + .map(ProductHealthfood::getHealthFood) + .toList(); + + return toResponseFromHealthFoods(healthFoods); + } + + public static HealthFoodKeywordResponse toResponseFromHealthFoods(List healthFoods) { + List types = extractTypes(healthFoods); + return HealthFoodKeywordResponse.builder() .types(types) .build(); } + + private static List extractTypes(List healthFoods) { + return healthFoods.stream() + .flatMap(healthFood -> healthFood.getHealthFoodKeywords().stream()) + .map(HealthFoodKeyword::getKeyword) + .map(Keyword::getType) + .distinct() + .toList(); + } } diff --git a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java index 0fb1c07..c098ba0 100644 --- a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java +++ b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java @@ -17,4 +17,7 @@ public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p LEFT JOIN p.cosmeticIngredients WHERE p.id = :id") Optional findByIdWithCosmeticIngredients(@Param("id") String id); + + @Query("SELECT p FROM Product p LEFT JOIN FETCH p.healthFoods ph LEFT JOIN FETCH ph.healthFood WHERE p.id = :id") + Optional findByIdWithHealthFoods(@Param("id") String productId); } From 3409c7f0c80e5018cdfe1d7e61ee1e894b2e532e Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 25 May 2025 17:32:19 +0900 Subject: [PATCH 243/309] =?UTF-8?q?#65=20feat:=20food=20enum=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/product/application/ProductService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 5538e31..0ad6460 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -4,6 +4,7 @@ import com.webeye.backend.allergy.type.AllergyType; import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.nutrition.dto.response.NutrientResponse; +import com.webeye.backend.product.domain.type.ProductType; import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; @@ -51,6 +52,7 @@ public ProductResponse analyzeFoodProduct(FoodProductAnalysisRequest request) { } Product product = Product.builder() .id(request.productId()) + .productType(ProductType.FOOD) .build(); productRepository.save(product); From 08df8201288276beebc2b00fd6059418fae95f8c Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 25 May 2025 17:50:54 +0900 Subject: [PATCH 244/309] =?UTF-8?q?#65=20remove:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/review/domain/Review.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/domain/Review.java b/src/main/java/com/webeye/backend/review/domain/Review.java index c7e3e7a..b8cd1f9 100644 --- a/src/main/java/com/webeye/backend/review/domain/Review.java +++ b/src/main/java/com/webeye/backend/review/domain/Review.java @@ -8,9 +8,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.Arrays; -import java.util.List; - @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -56,8 +53,4 @@ public void associateWithProduct(Product product) { product.associateWithReview(this); } } - - public List getKeywordList() { - return Arrays.asList(this.keywords.split(",")); - } } From 9f8b328b2b9f5211c18481d676f3e6af1a0a883d Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 25 May 2025 17:55:57 +0900 Subject: [PATCH 245/309] =?UTF-8?q?#65=20fix:=20saveProductHealthFood=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HealthFoodService.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index abe96f7..8f8d39e 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -1,9 +1,6 @@ package com.webeye.backend.healthfood.application; import com.webeye.backend.healthfood.domain.HealthFood; -import com.webeye.backend.healthfood.domain.HealthFoodKeyword; -import com.webeye.backend.healthfood.domain.Keyword; -import com.webeye.backend.healthfood.domain.type.HealthFoodType; import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; import com.webeye.backend.healthfood.dto.HealthFoodKeywordResponse; import com.webeye.backend.healthfood.dto.HealthFoodResponse; @@ -25,6 +22,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; @Slf4j @@ -86,10 +84,16 @@ public HealthFoodAiResponse analyzeHealthFood(FoodProductAnalysisRequest request @Transactional public void saveProductHealthFood(Product product, List healthFoods) { - List productHealthFoods = healthFoods.stream() - .map(healthFood -> ProductHealthFoodMapper.toEntity(product, healthFood)) - .toList(); + List productHealthFoods = new ArrayList<>(); + + for (HealthFood healthFood : healthFoods) { + ProductHealthfood productHealthfood = ProductHealthFoodMapper.toEntity(product, healthFood); + product.addHealthFood(productHealthfood); + healthFood.addProduct(productHealthfood); + + productHealthFoods.add(productHealthfood); + } productHealthFoodRepository.saveAll(productHealthFoods); } From 041ae85da23b7902b7be44446494a891a629507a Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 25 May 2025 18:03:21 +0900 Subject: [PATCH 246/309] =?UTF-8?q?#65=20remove:=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/healthfood/domain/HealthFood.java | 7 +------ .../backend/healthfood/domain/HealthFoodKeyword.java | 8 -------- .../com/webeye/backend/healthfood/domain/Keyword.java | 5 ----- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java index c83bac9..d7b47b8 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java @@ -28,7 +28,7 @@ public class HealthFood extends BaseEntity { private String functionality; @OneToMany(mappedBy = "healthFood", cascade = CascadeType.ALL) - private List healthFoodKeywords= new ArrayList<>(); + private List healthFoodKeywords = new ArrayList<>(); @OneToMany(mappedBy = "healthFood", cascade = CascadeType.ALL) private List healthfoods = new ArrayList<>(); @@ -39,11 +39,6 @@ public HealthFood(String itemName, String functionality) { this.functionality = functionality; } - public void addKeyword(HealthFoodKeyword healthFoodKeyword) { - healthFoodKeywords.add(healthFoodKeyword); - healthFoodKeyword.associateWithHealthFood(this); - } - public void addProduct(ProductHealthfood healthFood) { healthfoods.add(healthFood); healthFood.associateWithHealthFood(this); diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java index 72adee2..777a217 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFoodKeyword.java @@ -30,12 +30,4 @@ public HealthFoodKeyword(HealthFood healthFood, Keyword keyword) { this.healthFood = healthFood; this.keyword = keyword; } - - public void associateWithHealthFood(HealthFood healthFood) { - this.healthFood = healthFood; - } - - public void associateWithKeyword(Keyword keyword) { - this.keyword = keyword; - } } diff --git a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java index b4e67b3..313f95b 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/Keyword.java @@ -31,9 +31,4 @@ public class Keyword { public Keyword(HealthFoodType type) { this.type = type; } - - public void addHealthFood(HealthFoodKeyword healthFoodKeyword) { - healthFoodKeywords.add(healthFoodKeyword); - healthFoodKeyword.associateWithKeyword(this); - } } From 2e2ab3f889c3251d4532043abfb332069833534e Mon Sep 17 00:00:00 2001 From: zyovn Date: Sun, 25 May 2025 18:22:43 +0900 Subject: [PATCH 247/309] =?UTF-8?q?#65=20fix:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cosmetic/application/CosmeticService.java | 92 +++++++++++++++++- .../application/CosmeticServiceImpl.java | 97 ------------------- .../backend/cosmetic/domain/Ingredient.java | 6 +- .../persistence/init/IngredientInit.java | 3 +- .../product/persistent/ProductRepository.java | 2 +- 5 files changed, 93 insertions(+), 107 deletions(-) delete mode 100644 src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java index 65f452b..734dd38 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java @@ -1,8 +1,96 @@ package com.webeye.backend.cosmetic.application; +import com.webeye.backend.cosmetic.domain.CosmeticIngredient; +import com.webeye.backend.cosmetic.domain.Ingredient; +import com.webeye.backend.cosmetic.domain.type.IngredientType; import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; +import com.webeye.backend.cosmetic.infrastructure.mapper.CosmeticIngredientMapper; +import com.webeye.backend.cosmetic.infrastructure.persistence.CosmeticIngredientRepository; +import com.webeye.backend.cosmetic.infrastructure.persistence.IngredientRepository; +import com.webeye.backend.global.error.BusinessException; +import com.webeye.backend.global.error.ErrorCode; +import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.domain.type.ProductType; import com.webeye.backend.product.dto.request.ProductAnalysisRequest; +import com.webeye.backend.product.persistent.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -public interface CosmeticService { - CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request); +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CosmeticService { + + private final OpenAiClient openAiClient; + private final ImageUrlExtractor imageUrlExtractor; + private final ProductRepository productRepository; + private final IngredientRepository ingredientRepository; + private final CosmeticIngredientRepository cosmeticIngredientRepository; + + @Transactional + public CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request) { + Product product = findOrCreateProduct(request.productId()); + + // DB에 있을 경우, DB에서 조회 후 호출 + if (product.getCosmeticIngredients() != null && !product.getCosmeticIngredients().isEmpty()) { + return CosmeticIngredientMapper.toResponse(product.getCosmeticIngredients()); + } + + CosmeticResponse response = openAiClient.explainCosmetic( + imageUrlExtractor.extractImageUrlFromHtml(request.html()) + ); + + Map resultMap = convertToEnumMap(response); + + resultMap.entrySet().stream() + .filter(Map.Entry::getValue) + .forEach(entry -> { + Ingredient ingredient = ingredientRepository.findByIngredientType(entry.getKey()) + .orElseThrow(() -> new BusinessException(ErrorCode.COSMETIC_INGREDIENT_NOT_FOUND)); + + CosmeticIngredient cosmeticIngredient = CosmeticIngredientMapper.toEntity(product, ingredient); + cosmeticIngredientRepository.save(cosmeticIngredient); + }); + + return response; + } + + @Transactional + public Product findOrCreateProduct(String productId) { + return productRepository.findByIdWithCosmeticIngredients(productId) + .orElseGet(() -> productRepository.save( + Product.builder() + .id(productId) + .productType(ProductType.COSMETIC) + .build())); + } + + private Map convertToEnumMap(CosmeticResponse response) { + return Map.ofEntries( + Map.entry(IngredientType.AVOBENZONE, response.avobenzone()), + Map.entry(IngredientType.ISOPROPYL_ALCOHOL, response.isopropylAlcohol()), + Map.entry(IngredientType.SODIUM_LAURYL_SULFATE, response.sodiumLaurylSulfate()), + Map.entry(IngredientType.TRIETHANOLAMINE, response.triethanolamine()), + Map.entry(IngredientType.POLYETHYLENE_GLYCOL, response.polyethyleneGlycol()), + Map.entry(IngredientType.SYNTHETIC_COLORANT, response.syntheticColorant()), + Map.entry(IngredientType.ISOPROPYL_METHYLPHENOL, response.isopropylMethylphenol()), + Map.entry(IngredientType.SORBIC_ACID, response.sorbicAcid()), + Map.entry(IngredientType.HORMONE, response.hormone()), + Map.entry(IngredientType.DIBUTYL_HYDROXYTOLUENE, response.dibutylHydroxyToluene()), + Map.entry(IngredientType.PARABENS, response.parabens()), + Map.entry(IngredientType.TRICLOSAN, response.triclosan()), + Map.entry(IngredientType.BUTYLATED_HYDROXYANISOLE, response.butylatedHydroxyanisole()), + Map.entry(IngredientType.OXYBENZONE, response.oxybenzone()), + Map.entry(IngredientType.IMIDAZOLIDINYL_UREA, response.imidazolidinylUrea()), + Map.entry(IngredientType.MINERAL_OIL, response.mineralOil()), + Map.entry(IngredientType.THYMOL, response.thymol()), + Map.entry(IngredientType.TRIISOPROPANOLAMINE, response.triisopropanolamine()), + Map.entry(IngredientType.SYNTHETIC_FRAGRANCE, response.syntheticFragrance()), + Map.entry(IngredientType.PHENOXYETHANOL, response.phenoxyethanol()) + ); + } } diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java deleted file mode 100644 index 4ae87ca..0000000 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticServiceImpl.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.webeye.backend.cosmetic.application; - -import com.webeye.backend.cosmetic.domain.CosmeticIngredient; -import com.webeye.backend.cosmetic.domain.Ingredient; -import com.webeye.backend.cosmetic.domain.type.IngredientType; -import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; -import com.webeye.backend.cosmetic.infrastructure.mapper.CosmeticIngredientMapper; -import com.webeye.backend.cosmetic.infrastructure.persistence.CosmeticIngredientRepository; -import com.webeye.backend.cosmetic.infrastructure.persistence.IngredientRepository; -import com.webeye.backend.global.error.BusinessException; -import com.webeye.backend.global.error.ErrorCode; -import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; -import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; -import com.webeye.backend.product.domain.Product; -import com.webeye.backend.product.domain.type.ProductType; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; -import com.webeye.backend.product.persistent.ProductRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Map; - -@Service -@RequiredArgsConstructor -public class CosmeticServiceImpl implements CosmeticService { - - private final OpenAiClient openAiClient; - private final ImageUrlExtractor imageUrlExtractor; - private final ProductRepository productRepository; - private final IngredientRepository ingredientRepository; - private final CosmeticIngredientRepository cosmeticIngredientRepository; - - @Override - @Transactional - public CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request) { - Product product = findOrCreateProduct(request.productId()); - - // DB에 있을 경우, DB에서 조회 후 호출 - if (product.getCosmeticIngredients() != null && !product.getCosmeticIngredients().isEmpty()) { - return CosmeticIngredientMapper.toResponse(product.getCosmeticIngredients()); - } - - CosmeticResponse response = openAiClient.explainCosmetic( - imageUrlExtractor.extractImageUrlFromHtml(request.html()) - ); - - Map resultMap = convertToEnumMap(response); - - resultMap.entrySet().stream() - .filter(Map.Entry::getValue) - .forEach(entry -> { - Ingredient ingredient = ingredientRepository.findByIngredientType(entry.getKey()) - .orElseThrow(() -> new BusinessException(ErrorCode.COSMETIC_INGREDIENT_NOT_FOUND)); - - CosmeticIngredient cosmeticIngredient = CosmeticIngredientMapper.toEntity(product, ingredient); - cosmeticIngredientRepository.save(cosmeticIngredient); - }); - - return response; - } - - @Transactional - public Product findOrCreateProduct(String productId) { - return productRepository.findByIdWithCosmeticIngredients(productId) - .orElseGet(() -> productRepository.save( - Product.builder() - .id(productId) - .productType(ProductType.COSMETIC) - .build())); - } - - private Map convertToEnumMap(CosmeticResponse response) { - return Map.ofEntries( - Map.entry(IngredientType.AVOBENZONE, response.avobenzone()), - Map.entry(IngredientType.ISOPROPYL_ALCOHOL, response.isopropylAlcohol()), - Map.entry(IngredientType.SODIUM_LAURYL_SULFATE, response.sodiumLaurylSulfate()), - Map.entry(IngredientType.TRIETHANOLAMINE, response.triethanolamine()), - Map.entry(IngredientType.POLYETHYLENE_GLYCOL, response.polyethyleneGlycol()), - Map.entry(IngredientType.SYNTHETIC_COLORANT, response.syntheticColorant()), - Map.entry(IngredientType.ISOPROPYL_METHYLPHENOL, response.isopropylMethylphenol()), - Map.entry(IngredientType.SORBIC_ACID, response.sorbicAcid()), - Map.entry(IngredientType.HORMONE, response.hormone()), - Map.entry(IngredientType.DIBUTYL_HYDROXYTOLUENE, response.dibutylHydroxyToluene()), - Map.entry(IngredientType.PARABENS, response.parabens()), - Map.entry(IngredientType.TRICLOSAN, response.triclosan()), - Map.entry(IngredientType.BUTYLATED_HYDROXYANISOLE, response.butylatedHydroxyanisole()), - Map.entry(IngredientType.OXYBENZONE, response.oxybenzone()), - Map.entry(IngredientType.IMIDAZOLIDINYL_UREA, response.imidazolidinylUrea()), - Map.entry(IngredientType.MINERAL_OIL, response.mineralOil()), - Map.entry(IngredientType.THYMOL, response.thymol()), - Map.entry(IngredientType.TRIISOPROPANOLAMINE, response.triisopropanolamine()), - Map.entry(IngredientType.SYNTHETIC_FRAGRANCE, response.syntheticFragrance()), - Map.entry(IngredientType.PHENOXYETHANOL, response.phenoxyethanol()) - ); - } -} diff --git a/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java b/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java index ef137a9..de7ced0 100644 --- a/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java +++ b/src/main/java/com/webeye/backend/cosmetic/domain/Ingredient.java @@ -29,11 +29,7 @@ public class Ingredient extends BaseEntity { private List cosmeticIngredients = new ArrayList<>(); @Builder - public Ingredient( - Long id, - IngredientType ingredientType - ) { - this.id = id; + public Ingredient(IngredientType ingredientType) { this.ingredientType = ingredientType; } } diff --git a/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/init/IngredientInit.java b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/init/IngredientInit.java index 1c03db8..5a29198 100644 --- a/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/init/IngredientInit.java +++ b/src/main/java/com/webeye/backend/cosmetic/infrastructure/persistence/init/IngredientInit.java @@ -4,7 +4,6 @@ import com.webeye.backend.cosmetic.domain.type.IngredientType; import com.webeye.backend.cosmetic.infrastructure.persistence.IngredientRepository; import com.webeye.backend.global.util.DummyDataInit; -import lombok.Builder; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; @@ -19,7 +18,7 @@ public class IngredientInit implements ApplicationRunner { private final IngredientRepository ingredientRepository; - @Builder + @Override public void run(ApplicationArguments args) { for (IngredientType ingredientType : IngredientType.values()) { if (ingredientRepository.findByIngredientType(ingredientType).isEmpty()) { diff --git a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java index c098ba0..bdf3412 100644 --- a/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java +++ b/src/main/java/com/webeye/backend/product/persistent/ProductRepository.java @@ -15,7 +15,7 @@ public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p LEFT JOIN FETCH p.review WHERE p.id = :id") Optional findByIdWithReview(@Param("id") String id); - @Query("SELECT p FROM Product p LEFT JOIN p.cosmeticIngredients WHERE p.id = :id") + @Query("SELECT p FROM Product p LEFT JOIN FETCH p.cosmeticIngredients WHERE p.id = :id") Optional findByIdWithCosmeticIngredients(@Param("id") String id); @Query("SELECT p FROM Product p LEFT JOIN FETCH p.healthFoods ph LEFT JOIN FETCH ph.healthFood WHERE p.id = :id") From aab893d26c6ddd8b04e1fc1cd7f11334c96ccf29 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 16:20:53 +0900 Subject: [PATCH 248/309] =?UTF-8?q?#84=20feat:=20ProductDetail=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20Product=EC=99=80=EC=9D=98=20=EA=B4=80?= =?UTF-8?q?=EA=B3=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/product/domain/Product.java | 3 ++ .../backend/product/domain/ProductDetail.java | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/main/java/com/webeye/backend/product/domain/ProductDetail.java diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 7449207..05127d7 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -39,6 +39,9 @@ public class Product extends BaseEntity { @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) private List healthFoods = new ArrayList<>(); + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + private List details = new ArrayList<>(); + @Builder public Product( String id, diff --git a/src/main/java/com/webeye/backend/product/domain/ProductDetail.java b/src/main/java/com/webeye/backend/product/domain/ProductDetail.java new file mode 100644 index 0000000..6b9e6f6 --- /dev/null +++ b/src/main/java/com/webeye/backend/product/domain/ProductDetail.java @@ -0,0 +1,46 @@ +package com.webeye.backend.product.domain; + + +import com.webeye.backend.global.domain.BaseEntity; +import com.webeye.backend.product.domain.type.OutlineType; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "product_detail", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"product_id", "type"}) + } +) +public class ProductDetail extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Enumerated(EnumType.STRING) + private OutlineType outline; + + private String content; + + @Builder + public ProductDetail(Product product, OutlineType outline, String content) { + this.product = product; + this.outline = outline; + this.content = content; + } + + public void associateWithProduct(Product product) { + this.product = product; + product.getDetails().add(this); + } +} From 3b9b55f30bd8d4a463b4b62a4d9f03531630cd54 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 16:25:15 +0900 Subject: [PATCH 249/309] =?UTF-8?q?#84=20refactor:=20product=20detail=20?= =?UTF-8?q?=EC=9D=B4=EC=82=AC=ED=95=A9=EB=8B=88=EB=8B=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 3 +-- .../webeye/backend/product/application/ProductService.java | 4 +++- src/main/java/com/webeye/backend/product/domain/Product.java | 1 + .../backend/product/presentation/ProductController.java | 3 +-- .../backend/product/presentation/swagger/ProductSwagger.java | 2 +- .../{product => productdetail}/domain/ProductDetail.java | 5 +++-- .../{product => productdetail}/domain/type/OutlineType.java | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) rename src/main/java/com/webeye/backend/{product => productdetail}/domain/ProductDetail.java (86%) rename src/main/java/com/webeye/backend/{product => productdetail}/domain/type/OutlineType.java (96%) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index b7e9afe..a71b55d 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -4,12 +4,11 @@ import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; import com.webeye.backend.imageanalysis.dto.response.ImageAnalysisResponse; -import com.webeye.backend.product.dto.request.ProductAnalysisRequest; import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; -import com.webeye.backend.product.domain.type.OutlineType; +import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; import com.webeye.backend.rawmaterial.dto.response.RawMaterialAiResponse; diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 0ad6460..d1f8a80 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -12,7 +12,7 @@ import com.webeye.backend.nutrition.application.NutrientRecommendationService; import com.webeye.backend.nutrition.dto.request.NutrientRecommendationRequest; import com.webeye.backend.product.domain.ProductAllergy; -import com.webeye.backend.product.domain.type.OutlineType; +import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.application.NutritionService; import com.webeye.backend.product.domain.Product; @@ -83,6 +83,8 @@ private NutrientResponse getNutrientResponse( } public DetailExplanationResponse analyzeProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { + + return openAiClient.explainProductDetail(outline, imageUrlExtractor.extractImageUrlFromHtml(request.html())); } } diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 05127d7..c82371d 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -3,6 +3,7 @@ import com.webeye.backend.cosmetic.domain.CosmeticIngredient; import com.webeye.backend.global.domain.BaseEntity; import com.webeye.backend.product.domain.type.ProductType; +import com.webeye.backend.productdetail.domain.ProductDetail; import com.webeye.backend.review.domain.Review; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/webeye/backend/product/presentation/ProductController.java b/src/main/java/com/webeye/backend/product/presentation/ProductController.java index 7f01175..4a947db 100644 --- a/src/main/java/com/webeye/backend/product/presentation/ProductController.java +++ b/src/main/java/com/webeye/backend/product/presentation/ProductController.java @@ -2,7 +2,7 @@ import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.domain.type.OutlineType; +import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.product.application.ProductService; import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; @@ -36,5 +36,4 @@ public SuccessResponse productDetailAnalysis( @PathVariable OutlineType outline, @Valid @RequestBody ProductDetailAnalysisRequest request) { return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, productService.analyzeProductDetail(outline, request)); } - } diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java index 7bd7885..d91e7ca 100644 --- a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -2,7 +2,7 @@ import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.domain.type.OutlineType; +import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.product.dto.response.ProductResponse; diff --git a/src/main/java/com/webeye/backend/product/domain/ProductDetail.java b/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java similarity index 86% rename from src/main/java/com/webeye/backend/product/domain/ProductDetail.java rename to src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java index 6b9e6f6..bece279 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductDetail.java +++ b/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java @@ -1,8 +1,9 @@ -package com.webeye.backend.product.domain; +package com.webeye.backend.productdetail.domain; import com.webeye.backend.global.domain.BaseEntity; -import com.webeye.backend.product.domain.type.OutlineType; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.productdetail.domain.type.OutlineType; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/webeye/backend/product/domain/type/OutlineType.java b/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java similarity index 96% rename from src/main/java/com/webeye/backend/product/domain/type/OutlineType.java rename to src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java index 53e9700..b060c53 100644 --- a/src/main/java/com/webeye/backend/product/domain/type/OutlineType.java +++ b/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java @@ -1,4 +1,4 @@ -package com.webeye.backend.product.domain.type; +package com.webeye.backend.productdetail.domain.type; import lombok.Getter; From cf02ed75e1f935377d0ec236cb95eb0405aa58ae Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 16:35:38 +0900 Subject: [PATCH 250/309] =?UTF-8?q?#84=20feat:=20ProductDetailRepository?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistent/ProductDetailRepository.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/java/com/webeye/backend/productdetail/persistent/ProductDetailRepository.java diff --git a/src/main/java/com/webeye/backend/productdetail/persistent/ProductDetailRepository.java b/src/main/java/com/webeye/backend/productdetail/persistent/ProductDetailRepository.java new file mode 100644 index 0000000..857cc09 --- /dev/null +++ b/src/main/java/com/webeye/backend/productdetail/persistent/ProductDetailRepository.java @@ -0,0 +1,9 @@ +package com.webeye.backend.productdetail.persistent; + +import com.webeye.backend.productdetail.domain.ProductDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductDetailRepository extends JpaRepository { +} From f98beec2c4312c70a500b5647806777c43024755 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 16:46:09 +0900 Subject: [PATCH 251/309] =?UTF-8?q?#84=20refactor:=20ImageUrlExtractor=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=98=EA=B3=A0=20=EB=A9=94=EC=84=9C=EB=93=9C=20sta?= =?UTF-8?q?tic=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/allergy/application/AllergyService.java | 4 ++-- .../webeye/backend/cosmetic/application/CosmeticService.java | 3 +-- .../backend/healthfood/application/HealthFoodService.java | 3 +-- .../imageanalysis/infrastructure/ImageUrlExtractor.java | 4 +--- .../backend/nutrition/application/NutritionService.java | 3 +-- .../webeye/backend/product/application/ProductService.java | 5 +---- 6 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java index 95646c1..7096e12 100644 --- a/src/main/java/com/webeye/backend/allergy/application/AllergyService.java +++ b/src/main/java/com/webeye/backend/allergy/application/AllergyService.java @@ -11,15 +11,15 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + @Service @RequiredArgsConstructor public class AllergyService { private final OpenAiClient openAiClient; - private final ImageUrlExtractor imageUrlExtractor; private final ProductRepository productRepository; public AllergyAiResponse analyzeAllergy(FoodProductAnalysisRequest request) { - return openAiClient.explainAllergy(imageUrlExtractor.extractImageUrlFromHtml(request.html())); + return openAiClient.explainAllergy(ImageUrlExtractor.extractImageUrlFromHtml(request.html())); } @Transactional diff --git a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java index 734dd38..6c31f28 100644 --- a/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java +++ b/src/main/java/com/webeye/backend/cosmetic/application/CosmeticService.java @@ -26,7 +26,6 @@ public class CosmeticService { private final OpenAiClient openAiClient; - private final ImageUrlExtractor imageUrlExtractor; private final ProductRepository productRepository; private final IngredientRepository ingredientRepository; private final CosmeticIngredientRepository cosmeticIngredientRepository; @@ -41,7 +40,7 @@ public CosmeticResponse analyzeCosmetic(ProductAnalysisRequest request) { } CosmeticResponse response = openAiClient.explainCosmetic( - imageUrlExtractor.extractImageUrlFromHtml(request.html()) + ImageUrlExtractor.extractImageUrlFromHtml(request.html()) ); Map resultMap = convertToEnumMap(response); diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 8f8d39e..a687a27 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -37,7 +37,6 @@ public class HealthFoodService { private String serviceKey; private final OpenAiClient openAiClient; - private final ImageUrlExtractor imageUrlExtractor; private final HealthFoodClient healthFoodClient; private final ProductRepository productRepository; private final HealthFoodRepository healthFoodRepository; @@ -79,7 +78,7 @@ public HealthFoodKeywordResponse analyzeAndSaveHealthFood(FoodProductAnalysisReq } public HealthFoodAiResponse analyzeHealthFood(FoodProductAnalysisRequest request) { - return openAiClient.explainHealthFood(imageUrlExtractor.extractImageUrlFromHtml(request.html())); + return openAiClient.explainHealthFood(ImageUrlExtractor.extractImageUrlFromHtml(request.html())); } @Transactional diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java index e072401..768d1dc 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -4,7 +4,6 @@ import com.webeye.backend.global.error.ErrorCode; import groovy.json.StringEscapeUtils; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; @@ -12,9 +11,8 @@ import java.util.regex.Pattern; @Slf4j -@Component public class ImageUrlExtractor { - public List extractImageUrlFromHtml(String html) { + public static List extractImageUrlFromHtml(String html) { html = StringEscapeUtils.unescapeJava(html); Pattern pattern = Pattern.compile("]+src=[\"'](//[^\"']+)[\"']"); diff --git a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java index ab73618..3832d1f 100644 --- a/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java +++ b/src/main/java/com/webeye/backend/nutrition/application/NutritionService.java @@ -24,7 +24,6 @@ @Transactional(readOnly = true) public class NutritionService { private final OpenAiClient openAiClient; - private final ImageUrlExtractor imageUrlExtractor; private final RawMaterialService rawMaterialService; private final NutrientRepository nutrientRepository; @@ -36,7 +35,7 @@ public Nutrient findByType(NutrientType type) { @Transactional public void saveProductNutrition(Product product, FoodProductAnalysisRequest request) { - NutritionAiResponse response = openAiClient.explainNutrition(imageUrlExtractor.extractImageUrlFromHtml(request.html())); + NutritionAiResponse response = openAiClient.explainNutrition(ImageUrlExtractor.extractImageUrlFromHtml(request.html())); if (Boolean.TRUE.equals(response.isNutrientIncluded())) { product.setNutrientReferenceAmount(response.nutrientReferenceAmount()); Map nutrientMap = extractNutrientMap(response); diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index d1f8a80..e262d80 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -35,7 +35,6 @@ public class ProductService { private final NutrientRecommendationService nutrientRecommendationService; private final AllergyService allergyService; private final OpenAiClient openAiClient; - private final ImageUrlExtractor imageUrlExtractor; private final ProductRepository productRepository; @@ -83,8 +82,6 @@ private NutrientResponse getNutrientResponse( } public DetailExplanationResponse analyzeProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { - - - return openAiClient.explainProductDetail(outline, imageUrlExtractor.extractImageUrlFromHtml(request.html())); + return openAiClient.explainProductDetail(outline, ImageUrlExtractor.extractImageUrlFromHtml(request.html())); } } From e698803cffc61ce8eca81c540284a4c2bcfcbcf7 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 17:59:03 +0900 Subject: [PATCH 252/309] =?UTF-8?q?#84=20fix:=20product=20detail=20content?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EA=B0=80=EB=8A=A5=20=EA=B8=B8=EC=9D=B4?= =?UTF-8?q?=20=EB=8A=98=EB=A6=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/productdetail/domain/ProductDetail.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java b/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java index bece279..9e2dbda 100644 --- a/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java +++ b/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java @@ -1,6 +1,5 @@ package com.webeye.backend.productdetail.domain; - import com.webeye.backend.global.domain.BaseEntity; import com.webeye.backend.product.domain.Product; import com.webeye.backend.productdetail.domain.type.OutlineType; @@ -31,6 +30,7 @@ public class ProductDetail extends BaseEntity { @Enumerated(EnumType.STRING) private OutlineType outline; + @Column(columnDefinition = "TEXT") private String content; @Builder From e9e5f93b654610e0c4952ec77d316d108ce06187 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 17:59:29 +0900 Subject: [PATCH 253/309] #84 refactor: productType nullable = true --- src/main/java/com/webeye/backend/product/domain/Product.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index c82371d..70e25e4 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -22,7 +22,7 @@ public class Product extends BaseEntity { private Integer nutrientReferenceAmount; @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(nullable = true) private ProductType productType; @OneToOne(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) From 9fa7f7ac71a797d2ea8fc8954ca792bd6a20b72d Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:00:41 +0900 Subject: [PATCH 254/309] =?UTF-8?q?#84=20feat:=20Product=20Detail=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20outline=EC=97=90=20=EB=8C=80=ED=95=B4=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/OpenAiClient.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index a71b55d..b57082a 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -11,6 +11,7 @@ import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; +import com.webeye.backend.productdetail.dto.AllDetailExplanationResponse; import com.webeye.backend.rawmaterial.dto.response.RawMaterialAiResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -54,6 +55,26 @@ public DetailExplanationResponse explainProductDetail(OutlineType outline, List< return callWithStructuredOutput(urls, prompt, DetailExplanationResponse.class); } + public AllDetailExplanationResponse explainProductAllDetail(List urls) { + String system = """ + You are an expert in providing detailed explanations about products based on images. + When a user provides a product description image along with the key outline of that description, you should offer a clear and detailed explanation of that element. + In this explanation, you must provide very detailed information about that element from the image. Answer in Korean. + """; + String user = String.format(""" + Please generate a detailed explanation of the provided key. + Provide your answer following the FORMAT I provided. I will specify what content should be included for each key. + main: %s + usage: %s + warning: %s + specs: %s + certification: %s + """, OutlineType.MAIN.getPrompt(), OutlineType.USAGE.getPrompt(), OutlineType.WARNING.getPrompt(), OutlineType.SPECS.getPrompt(), OutlineType.CERTIFICATION.getPrompt()); + + ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); + return callWithStructuredOutput(urls, prompt, AllDetailExplanationResponse.class); + } + public AllergyAiResponse explainAllergy(List urls) { String system = """ You are an OCR assistant that extracts and detects allergenic ingredients from Korean product label images. Always treat partial matches inside compound words as valid if they contain the full Korean name of an allergen. @@ -256,7 +277,6 @@ public RawMaterialAiResponse explainRawMaterial(FoodProductAnalysisRequest reque .name(result) .build(); } - } From 0c2df20f3c5d58ec495ad2a4d86ffa54f66bebbd Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:01:49 +0900 Subject: [PATCH 255/309] =?UTF-8?q?#84=20refactor:=20ProductDetail=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=98=EB=8A=94=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/AllDetailExplanationResponse.java | 13 +++++++++ .../presentation/ProductDetailController.java | 28 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/main/java/com/webeye/backend/productdetail/dto/AllDetailExplanationResponse.java create mode 100644 src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java diff --git a/src/main/java/com/webeye/backend/productdetail/dto/AllDetailExplanationResponse.java b/src/main/java/com/webeye/backend/productdetail/dto/AllDetailExplanationResponse.java new file mode 100644 index 0000000..2fd3da8 --- /dev/null +++ b/src/main/java/com/webeye/backend/productdetail/dto/AllDetailExplanationResponse.java @@ -0,0 +1,13 @@ +package com.webeye.backend.productdetail.dto; + +import lombok.Builder; + +@Builder +public record AllDetailExplanationResponse( + String main, + String usage, + String warning, + String specs, + String certification +) { +} diff --git a/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java b/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java new file mode 100644 index 0000000..9efc29c --- /dev/null +++ b/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java @@ -0,0 +1,28 @@ +package com.webeye.backend.productdetail.presentation; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; +import com.webeye.backend.product.dto.response.DetailExplanationResponse; +import com.webeye.backend.productdetail.application.ProductDetailService; +import com.webeye.backend.productdetail.domain.type.OutlineType; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import static com.webeye.backend.global.dto.response.type.SuccessCode.PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/v1/product-detail") +public class ProductDetailController { + private final ProductDetailService productDetailService; + +// @Override + @ResponseStatus(HttpStatus.OK) + @PostMapping(value = "/{outline}") + public SuccessResponse productDetailAnalysis( + @PathVariable OutlineType outline, @Valid @RequestBody ProductDetailAnalysisRequest request) { + return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, productDetailService.analyzeProductDetail(outline, request)); + } +} From eb09705cb0c865bdbd0e3ec751c1e05cdb2a40b3 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:03:16 +0900 Subject: [PATCH 256/309] #84 feat: ProductDetailService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 저장된 Product Detail이 존재하지 않으면 저장합니다. 요청된 Outline에 해당하는 Product Detail을 제공합니다. --- .../request/ProductDetailAnalysisRequest.java | 5 +- .../application/ProductDetailService.java | 73 +++++++++++++++++++ .../domain/type/OutlineType.java | 10 +-- 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java index 748341d..8cc48b2 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java @@ -5,9 +5,12 @@ import jakarta.validation.constraints.Pattern; import lombok.Builder; -@Schema(description = "상품 설명 이미지의 HTML") +@Schema(description = "상품 설명 요청") @Builder public record ProductDetailAnalysisRequest( + @Schema(description = "상품 ID") + String productId, + @Schema(description = "상품 상세 정보 HTML") @NotEmpty(message = "상품 상세 정보의 HTML은 비어있을 수 없습니다.") @Pattern(regexp = ".*.*", message = "HTML에는 최소한 하나의 이미지 태그가 포함되어야 합니다.") diff --git a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java new file mode 100644 index 0000000..42e53ff --- /dev/null +++ b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java @@ -0,0 +1,73 @@ +package com.webeye.backend.productdetail.application; + +import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; +import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; +import com.webeye.backend.product.domain.Product; +import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; +import com.webeye.backend.product.dto.response.DetailExplanationResponse; +import com.webeye.backend.product.persistent.ProductRepository; +import com.webeye.backend.productdetail.domain.ProductDetail; +import com.webeye.backend.productdetail.domain.type.OutlineType; +import com.webeye.backend.productdetail.dto.AllDetailExplanationResponse; +import com.webeye.backend.productdetail.persistent.ProductDetailRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ProductDetailService { + private final OpenAiClient openAiClient; + + private final ProductDetailRepository productDetailRepository; + private final ProductRepository productRepository; + + @Transactional + public DetailExplanationResponse analyzeProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { + Optional productDetailOpt = productDetailRepository.findByProductIdAndOutline(request.productId(), outline); + if (productDetailOpt.isEmpty()) { + AllDetailExplanationResponse details = openAiClient.explainProductAllDetail(ImageUrlExtractor.extractImageUrlFromHtml(request.html())); + log.info("product detail response:\n{}", details.toString()); + + saveProductDetails(request.productId(), details); + return new DetailExplanationResponse(getContentByOutline(details, outline)); + } + return new DetailExplanationResponse(productDetailOpt.get().getContent()); + } + + @Transactional + public void saveProductDetails(String id, AllDetailExplanationResponse details) { + Product product = findOrCreateProduct(id); + ProductDetail main = ProductDetail.builder().product(product).outline(OutlineType.MAIN).content(details.main()).build(); + ProductDetail usage = ProductDetail.builder().product(product).outline(OutlineType.USAGE).content(details.usage()).build(); + ProductDetail warning = ProductDetail.builder().product(product).outline(OutlineType.WARNING).content(details.warning()).build(); + ProductDetail specs = ProductDetail.builder().product(product).outline(OutlineType.SPECS).content(details.specs()).build(); + ProductDetail certification = ProductDetail.builder().product(product).outline(OutlineType.CERTIFICATION).content(details.certification()).build(); + + productDetailRepository.saveAll(List.of(main, usage, warning, specs, certification)); + } + + @Transactional + public Product findOrCreateProduct(String productId) { + return productRepository.findById(productId) + .orElseGet(() -> productRepository.save( + Product.builder() + .id(productId) + .build())); + } + + private String getContentByOutline(AllDetailExplanationResponse details, OutlineType outline) { + return switch (outline) { + case MAIN -> details.main(); + case USAGE -> details.usage(); + case WARNING -> details.warning(); + case SPECS -> details.specs(); + case CERTIFICATION -> details.certification(); + }; + } +} diff --git a/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java b/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java index b060c53..baecf9a 100644 --- a/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java +++ b/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java @@ -4,11 +4,11 @@ @Getter public enum OutlineType { - MAIN("Analyze the product description image I send and provide information about the main ingredients, materials, functions, and components of the product. If it is a food item, nutritional information is required. Respond in Korean. (e.g., ingredients/nutritional content, material, functionality, components, country of origin, manufacturer)"), - USAGE("Analyze the product description image I send and provide information about how to use (consume/assemble/install/utilize) the product and the intended user. Respond in Korean. (e.g., consumption method, usage steps, recommended users, age group, installation method)"), - WARNING("Analyze the product description image I send and provide information about storage methods, expiration date, safety precautions, allergies, cleaning, etc. If it is a food item, allergy information is required. Respond in Korean. (e.g., storage method, avoid direct sunlight, expiration date, washing, prohibitions)"), - SPECS("Analyze the product description image I send and provide information about the product’s color, size, weight, capacity, compatibility, and purchasing options. Respond in Korean. (e.g., size, weight, capacity, size options, color, option configuration, coverage)"), - CERTIFICATION("Analyze the product description image I send and provide information about the product’s certifications, after-sales service, packaging, brand, delivery, and marks. Respond in Korean. (e.g., KC certification, GMP, vegan, KFDA, free A/S, packaging)") + MAIN("Analyze the product description image I send and provide information about the main ingredients, materials, functions, and components of the product. If it is a food item, nutritional information is required. (e.g., ingredients/nutritional content, material, functionality, components, country of origin, manufacturer)"), + USAGE("Analyze the product description image I send and provide information about how to use (consume/assemble/install/utilize) the product and the intended user. (e.g., consumption method, usage steps, recommended users, age group, installation method)"), + WARNING("Analyze the product description image I send and provide information about storage methods, expiration date, safety precautions, allergies, cleaning, etc. If it is a food item, allergy information is required. (e.g., storage method, avoid direct sunlight, expiration date, washing, prohibitions)"), + SPECS("Analyze the product description image I send and provide information about the product’s color, size, weight, capacity, compatibility, and purchasing options. (e.g., size, weight, capacity, size options, color, option configuration, coverage)"), + CERTIFICATION("Analyze the product description image I send and provide information about the product’s certifications, after-sales service, packaging, brand, delivery, and marks. (e.g., KC certification, GMP, vegan, KFDA, free A/S, packaging)") ; private final String prompt; From b7fd885f78ba1b3817b1fd99fc5603fdb65a5f85 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:03:47 +0900 Subject: [PATCH 257/309] #84 feat: findByProductIdAndOutline repository --- .../productdetail/persistent/ProductDetailRepository.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/webeye/backend/productdetail/persistent/ProductDetailRepository.java b/src/main/java/com/webeye/backend/productdetail/persistent/ProductDetailRepository.java index 857cc09..525655d 100644 --- a/src/main/java/com/webeye/backend/productdetail/persistent/ProductDetailRepository.java +++ b/src/main/java/com/webeye/backend/productdetail/persistent/ProductDetailRepository.java @@ -1,9 +1,13 @@ package com.webeye.backend.productdetail.persistent; import com.webeye.backend.productdetail.domain.ProductDetail; +import com.webeye.backend.productdetail.domain.type.OutlineType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface ProductDetailRepository extends JpaRepository { + Optional findByProductIdAndOutline(String productId, OutlineType outlineType); } From 4f3cae79cf39e39cb1dc2c3b4086274fb46fac68 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:08:22 +0900 Subject: [PATCH 258/309] #84 docs: product detail swagger --- .../global/dto/response/type/SuccessCode.java | 2 ++ .../presentation/ProductDetailController.java | 5 +-- .../swagger/ProductDetailSwagger.java | 33 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java diff --git a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java index cf2aa00..086c407 100644 --- a/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java +++ b/src/main/java/com/webeye/backend/global/dto/response/type/SuccessCode.java @@ -12,6 +12,8 @@ public enum SuccessCode { // product FOOD_PRODUCT_ANALYSIS_SUCCESS(200, "Product Analysis Success"), + + // product detail PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS(200, "Product detail explanation analysis success"), // explanation diff --git a/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java b/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java index 9efc29c..ffe217f 100644 --- a/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java +++ b/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java @@ -5,6 +5,7 @@ import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.productdetail.application.ProductDetailService; import com.webeye.backend.productdetail.domain.type.OutlineType; +import com.webeye.backend.productdetail.presentation.swagger.ProductDetailSwagger; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -15,10 +16,10 @@ @RequiredArgsConstructor @RestController @RequestMapping("/v1/product-detail") -public class ProductDetailController { +public class ProductDetailController implements ProductDetailSwagger { private final ProductDetailService productDetailService; -// @Override + @Override @ResponseStatus(HttpStatus.OK) @PostMapping(value = "/{outline}") public SuccessResponse productDetailAnalysis( diff --git a/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java b/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java new file mode 100644 index 0000000..388882c --- /dev/null +++ b/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java @@ -0,0 +1,33 @@ +package com.webeye.backend.productdetail.presentation.swagger; + +import com.webeye.backend.global.dto.response.SuccessResponse; +import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; +import com.webeye.backend.product.dto.response.DetailExplanationResponse; +import com.webeye.backend.productdetail.domain.type.OutlineType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "[제품 상세 설명]", description = "제품 상세 설명 관련 API") +public interface ProductDetailSwagger { + @Operation( + summary = "제품 주요 개요에 대한 상세 설명 추출", + description = "제품 ID와 상세 설명 이미지가 포함된 HTML, 개요를 입력받아 주요 요소에 대한 상세 설명을 추출합니다. " + + "MAIN: 주요정보, USAGE: 사용정보, WARNING: 주의 및 보관, SPECS: 규격 및 옵션, CERTIFICATION: 인증 및 기타" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "제품 주요 요소에 대한 상세 설명이 추출되었습니다." + ) + }) + SuccessResponse productDetailAnalysis( + @Parameter(in = ParameterIn.PATH, description = "상품 개요", required = true) + OutlineType outline, + @RequestBody ProductDetailAnalysisRequest request + ); +} From f4d80a9e0ddb8503b808f93aaab14bc7a07694ec Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:08:43 +0900 Subject: [PATCH 259/309] =?UTF-8?q?#84=20remove:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20API=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/presentation/ProductController.java | 9 --------- .../presentation/swagger/ProductSwagger.java | 17 ----------------- 2 files changed, 26 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/presentation/ProductController.java b/src/main/java/com/webeye/backend/product/presentation/ProductController.java index 4a947db..62b4765 100644 --- a/src/main/java/com/webeye/backend/product/presentation/ProductController.java +++ b/src/main/java/com/webeye/backend/product/presentation/ProductController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*; import static com.webeye.backend.global.dto.response.type.SuccessCode.FOOD_PRODUCT_ANALYSIS_SUCCESS; -import static com.webeye.backend.global.dto.response.type.SuccessCode.PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS; @RequiredArgsConstructor @RestController @@ -28,12 +27,4 @@ public class ProductController implements ProductSwagger { public SuccessResponse foodAnalysis(@Valid @RequestBody FoodProductAnalysisRequest request) { return SuccessResponse.of(FOOD_PRODUCT_ANALYSIS_SUCCESS, productService.analyzeFoodProduct(request)); } - - @Override - @ResponseStatus(HttpStatus.OK) - @PostMapping(value = "/analysis/{outline}") - public SuccessResponse productDetailAnalysis( - @PathVariable OutlineType outline, @Valid @RequestBody ProductDetailAnalysisRequest request) { - return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, productService.analyzeProductDetail(outline, request)); - } } diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java index d91e7ca..3e36aec 100644 --- a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -29,21 +29,4 @@ public interface ProductSwagger { SuccessResponse foodAnalysis( @RequestBody FoodProductAnalysisRequest request ); - - @Operation( - summary = "제품 주요 개요에 대한 상세 설명 추출", - description = "제품 상세 설명 이미지가 포함된 HTML과 개요를 입력받아 주요 요소에 대한 상세 설명을 추출합니다. " + - "MAIN: 주요정보, USAGE: 사용정보, WARNING: 주의 및 보관, SPECS: 규격 및 옵션, CERTIFICATION: 인증 및 기타" - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "제품 주요 요소에 대한 상세 설명이 추출되었습니다." - ) - }) - SuccessResponse productDetailAnalysis( - @Parameter(in = ParameterIn.PATH, description = "상품 개요", required = true) - OutlineType outline, - @RequestBody ProductDetailAnalysisRequest request - ); } From 4b0ef1ce697d7cb869f45b59cc9a9189e612b0a9 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:15:53 +0900 Subject: [PATCH 260/309] =?UTF-8?q?#84=20refactor:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/OpenAiClient.java | 18 ------------------ .../product/application/ProductService.java | 5 ----- .../presentation/ProductController.java | 3 --- 3 files changed, 26 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index b57082a..3a8a4c8 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -4,7 +4,6 @@ import com.webeye.backend.cosmetic.dto.response.CosmeticResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisRequest; import com.webeye.backend.imageanalysis.dto.response.ImageAnalysisResponse; -import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.healthfood.dto.HealthFoodAiResponse; import com.webeye.backend.imageanalysis.dto.request.ImageAnalysisPrompt; @@ -38,23 +37,6 @@ public class OpenAiClient { private final ChatClient chatClient; - public DetailExplanationResponse explainProductDetail(OutlineType outline, List urls) { - String system = """ - You are an expert in providing detailed explanations about products based on images. - When a user provides a product description image along with the key outline of that description, you should offer a clear and detailed explanation of that element. - In this explanation, you must provide very detailed information about that element from the image. Answer in Korean. - """; - - String user = String.format(""" - Key Outline: %s - Please generate a detailed explanation of the provided outline. - """, outline.getPrompt()); - - - ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); - return callWithStructuredOutput(urls, prompt, DetailExplanationResponse.class); - } - public AllDetailExplanationResponse explainProductAllDetail(List urls) { String system = """ You are an expert in providing detailed explanations about products based on images. diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index e262d80..055231e 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -34,7 +34,6 @@ public class ProductService { private final NutritionService nutritionService; private final NutrientRecommendationService nutrientRecommendationService; private final AllergyService allergyService; - private final OpenAiClient openAiClient; private final ProductRepository productRepository; @@ -80,8 +79,4 @@ private NutrientResponse getNutrientResponse( .builder().birthYear(request.birthYear()).gender(request.gender()).product(product).build())) .build(); } - - public DetailExplanationResponse analyzeProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { - return openAiClient.explainProductDetail(outline, ImageUrlExtractor.extractImageUrlFromHtml(request.html())); - } } diff --git a/src/main/java/com/webeye/backend/product/presentation/ProductController.java b/src/main/java/com/webeye/backend/product/presentation/ProductController.java index 62b4765..524613a 100644 --- a/src/main/java/com/webeye/backend/product/presentation/ProductController.java +++ b/src/main/java/com/webeye/backend/product/presentation/ProductController.java @@ -1,11 +1,8 @@ package com.webeye.backend.product.presentation; -import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.product.application.ProductService; -import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.product.dto.response.ProductResponse; import com.webeye.backend.product.presentation.swagger.ProductSwagger; import jakarta.validation.Valid; From 4b579dc4c7e498b3d84b4c1f94a75f62451b6d2d Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:55:04 +0900 Subject: [PATCH 261/309] #84 fix: @UniqueConstraint type -> outline --- .../com/webeye/backend/productdetail/domain/ProductDetail.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java b/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java index 9e2dbda..228f973 100644 --- a/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java +++ b/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java @@ -15,7 +15,7 @@ @Table( name = "product_detail", uniqueConstraints = { - @UniqueConstraint(columnNames = {"product_id", "type"}) + @UniqueConstraint(columnNames = {"product_id", "outline"}) } ) public class ProductDetail extends BaseEntity { From bff41a55867fbacb3dbffb5079f56f22bfd48f7c Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:56:44 +0900 Subject: [PATCH 262/309] =?UTF-8?q?#84=20refactor:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/product/presentation/swagger/ProductSwagger.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java index 3e36aec..714d7f9 100644 --- a/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java +++ b/src/main/java/com/webeye/backend/product/presentation/swagger/ProductSwagger.java @@ -1,14 +1,9 @@ package com.webeye.backend.product.presentation.swagger; -import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; -import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.product.dto.response.ProductResponse; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; From 3a2eeeadff9eb5b344a095456cc61120efa2e31b Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 18:58:12 +0900 Subject: [PATCH 263/309] =?UTF-8?q?#84=20feat:=20productId=20@NotBlank=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 --- .../product/dto/request/ProductDetailAnalysisRequest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java index 8cc48b2..3b63a22 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java @@ -1,6 +1,7 @@ package com.webeye.backend.product.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Pattern; import lombok.Builder; @@ -9,6 +10,7 @@ @Builder public record ProductDetailAnalysisRequest( @Schema(description = "상품 ID") + @NotBlank(message = "상품 ID는 비어있을 수 없습니다.") String productId, @Schema(description = "상품 상세 정보 HTML") From 8728f603ca7346bc06419187d2a4c2d9bc3936e0 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 19:07:49 +0900 Subject: [PATCH 264/309] =?UTF-8?q?#84=20refactor:=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EB=90=98=EB=8A=94=20ProductDetail=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ProductDetailService.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java index 42e53ff..5d0fbfb 100644 --- a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java +++ b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java @@ -43,13 +43,23 @@ public DetailExplanationResponse analyzeProductDetail(OutlineType outline, Produ @Transactional public void saveProductDetails(String id, AllDetailExplanationResponse details) { Product product = findOrCreateProduct(id); - ProductDetail main = ProductDetail.builder().product(product).outline(OutlineType.MAIN).content(details.main()).build(); - ProductDetail usage = ProductDetail.builder().product(product).outline(OutlineType.USAGE).content(details.usage()).build(); - ProductDetail warning = ProductDetail.builder().product(product).outline(OutlineType.WARNING).content(details.warning()).build(); - ProductDetail specs = ProductDetail.builder().product(product).outline(OutlineType.SPECS).content(details.specs()).build(); - ProductDetail certification = ProductDetail.builder().product(product).outline(OutlineType.CERTIFICATION).content(details.certification()).build(); - productDetailRepository.saveAll(List.of(main, usage, warning, specs, certification)); + List productDetails = List.of( + createProductDetail(product, OutlineType.MAIN, details.main()), + createProductDetail(product, OutlineType.USAGE, details.usage()), + createProductDetail(product, OutlineType.WARNING, details.warning()), + createProductDetail(product, OutlineType.SPECS, details.specs()), + createProductDetail(product, OutlineType.CERTIFICATION, details.certification()) + ); + productDetailRepository.saveAll(productDetails); + } + + private ProductDetail createProductDetail(Product product, OutlineType outline, String content) { + return ProductDetail.builder() + .product(product) + .outline(outline) + .content(content) + .build(); } @Transactional From 43a02e2fc7c821911d68a78e2fcdb5c60bb9229c Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 19:23:55 +0900 Subject: [PATCH 265/309] =?UTF-8?q?#84=20refactor:=20Product=20Detail=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EB=A7=90=ED=88=AC=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 3a8a4c8..c5367a0 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -41,7 +41,8 @@ public AllDetailExplanationResponse explainProductAllDetail(List urls) { String system = """ You are an expert in providing detailed explanations about products based on images. When a user provides a product description image along with the key outline of that description, you should offer a clear and detailed explanation of that element. - In this explanation, you must provide very detailed information about that element from the image. Answer in Korean. + In this explanation, you must provide very detailed information about that element from the image. Answer in Korean. + I'd like it to have a descriptive tone, like "부셔서 먹는 방식으로, 남녀노소 즐길 수 있는 간식입니다." """; String user = String.format(""" Please generate a detailed explanation of the provided key. From 3365f64104c397e3fff183ea3583da616e28e18c Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 19:24:55 +0900 Subject: [PATCH 266/309] #84 feat: addProductDetails() --- .../java/com/webeye/backend/product/domain/Product.java | 7 +++++++ .../productdetail/application/ProductDetailService.java | 1 + 2 files changed, 8 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index 70e25e4..a39d43f 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -76,6 +76,13 @@ public void addHealthFood(ProductHealthfood healthFood) { healthFood.associateWithProduct(this); } + public void addProductDetails(List details) { + details.forEach(detail -> { + this.details.add(detail); + detail.associateWithProduct(this); + }); + } + public void setNutrientReferenceAmount(Integer nutrientReferenceAmount) { this.nutrientReferenceAmount = nutrientReferenceAmount; } diff --git a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java index 5d0fbfb..03d8900 100644 --- a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java +++ b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java @@ -51,6 +51,7 @@ public void saveProductDetails(String id, AllDetailExplanationResponse details) createProductDetail(product, OutlineType.SPECS, details.specs()), createProductDetail(product, OutlineType.CERTIFICATION, details.certification()) ); + product.addProductDetails(productDetails); productDetailRepository.saveAll(productDetails); } From de33a58bdbff2dd64ca7900cbd306465c58c9553 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 19:30:53 +0900 Subject: [PATCH 267/309] =?UTF-8?q?#84=20refactor:=20ProductDetail=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20DTO=20package=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 2 +- .../webeye/backend/product/application/ProductService.java | 5 ----- .../productdetail/application/ProductDetailService.java | 6 +++--- .../dto/request/ProductDetailAnalysisRequest.java | 2 +- .../dto/{ => response}/AllDetailExplanationResponse.java | 2 +- .../dto/response/DetailExplanationResponse.java | 2 +- .../productdetail/presentation/ProductDetailController.java | 4 ++-- .../presentation/swagger/ProductDetailSwagger.java | 4 ++-- 8 files changed, 11 insertions(+), 16 deletions(-) rename src/main/java/com/webeye/backend/{product => productdetail}/dto/request/ProductDetailAnalysisRequest.java (93%) rename src/main/java/com/webeye/backend/productdetail/dto/{ => response}/AllDetailExplanationResponse.java (78%) rename src/main/java/com/webeye/backend/{product => productdetail}/dto/response/DetailExplanationResponse.java (65%) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index c5367a0..461fc30 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -10,7 +10,7 @@ import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.dto.response.NutritionAiResponse; -import com.webeye.backend.productdetail.dto.AllDetailExplanationResponse; +import com.webeye.backend.productdetail.dto.response.AllDetailExplanationResponse; import com.webeye.backend.rawmaterial.dto.response.RawMaterialAiResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 055231e..9ee9f3e 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -2,21 +2,16 @@ import com.webeye.backend.allergy.application.AllergyService; import com.webeye.backend.allergy.type.AllergyType; -import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.nutrition.dto.response.NutrientResponse; import com.webeye.backend.product.domain.type.ProductType; -import com.webeye.backend.product.dto.response.DetailExplanationResponse; import com.webeye.backend.global.error.BusinessException; import com.webeye.backend.global.error.ErrorCode; -import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.nutrition.application.NutrientRecommendationService; import com.webeye.backend.nutrition.dto.request.NutrientRecommendationRequest; import com.webeye.backend.product.domain.ProductAllergy; -import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.product.dto.request.FoodProductAnalysisRequest; import com.webeye.backend.nutrition.application.NutritionService; import com.webeye.backend.product.domain.Product; -import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; import com.webeye.backend.product.dto.response.ProductResponse; import com.webeye.backend.product.persistent.ProductRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java index 03d8900..86f575b 100644 --- a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java +++ b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java @@ -3,12 +3,12 @@ import com.webeye.backend.imageanalysis.infrastructure.ImageUrlExtractor; import com.webeye.backend.imageanalysis.infrastructure.OpenAiClient; import com.webeye.backend.product.domain.Product; -import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; -import com.webeye.backend.product.dto.response.DetailExplanationResponse; +import com.webeye.backend.productdetail.dto.request.ProductDetailAnalysisRequest; +import com.webeye.backend.productdetail.dto.response.DetailExplanationResponse; import com.webeye.backend.product.persistent.ProductRepository; import com.webeye.backend.productdetail.domain.ProductDetail; import com.webeye.backend.productdetail.domain.type.OutlineType; -import com.webeye.backend.productdetail.dto.AllDetailExplanationResponse; +import com.webeye.backend.productdetail.dto.response.AllDetailExplanationResponse; import com.webeye.backend.productdetail.persistent.ProductDetailRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java b/src/main/java/com/webeye/backend/productdetail/dto/request/ProductDetailAnalysisRequest.java similarity index 93% rename from src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java rename to src/main/java/com/webeye/backend/productdetail/dto/request/ProductDetailAnalysisRequest.java index 3b63a22..a929793 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/ProductDetailAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/productdetail/dto/request/ProductDetailAnalysisRequest.java @@ -1,4 +1,4 @@ -package com.webeye.backend.product.dto.request; +package com.webeye.backend.productdetail.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/com/webeye/backend/productdetail/dto/AllDetailExplanationResponse.java b/src/main/java/com/webeye/backend/productdetail/dto/response/AllDetailExplanationResponse.java similarity index 78% rename from src/main/java/com/webeye/backend/productdetail/dto/AllDetailExplanationResponse.java rename to src/main/java/com/webeye/backend/productdetail/dto/response/AllDetailExplanationResponse.java index 2fd3da8..0b5b5b7 100644 --- a/src/main/java/com/webeye/backend/productdetail/dto/AllDetailExplanationResponse.java +++ b/src/main/java/com/webeye/backend/productdetail/dto/response/AllDetailExplanationResponse.java @@ -1,4 +1,4 @@ -package com.webeye.backend.productdetail.dto; +package com.webeye.backend.productdetail.dto.response; import lombok.Builder; diff --git a/src/main/java/com/webeye/backend/product/dto/response/DetailExplanationResponse.java b/src/main/java/com/webeye/backend/productdetail/dto/response/DetailExplanationResponse.java similarity index 65% rename from src/main/java/com/webeye/backend/product/dto/response/DetailExplanationResponse.java rename to src/main/java/com/webeye/backend/productdetail/dto/response/DetailExplanationResponse.java index 42736ba..6e5ef88 100644 --- a/src/main/java/com/webeye/backend/product/dto/response/DetailExplanationResponse.java +++ b/src/main/java/com/webeye/backend/productdetail/dto/response/DetailExplanationResponse.java @@ -1,4 +1,4 @@ -package com.webeye.backend.product.dto.response; +package com.webeye.backend.productdetail.dto.response; import lombok.Builder; diff --git a/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java b/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java index ffe217f..67926c6 100644 --- a/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java +++ b/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java @@ -1,8 +1,8 @@ package com.webeye.backend.productdetail.presentation; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; -import com.webeye.backend.product.dto.response.DetailExplanationResponse; +import com.webeye.backend.productdetail.dto.request.ProductDetailAnalysisRequest; +import com.webeye.backend.productdetail.dto.response.DetailExplanationResponse; import com.webeye.backend.productdetail.application.ProductDetailService; import com.webeye.backend.productdetail.domain.type.OutlineType; import com.webeye.backend.productdetail.presentation.swagger.ProductDetailSwagger; diff --git a/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java b/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java index 388882c..57310a0 100644 --- a/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java +++ b/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java @@ -1,8 +1,8 @@ package com.webeye.backend.productdetail.presentation.swagger; import com.webeye.backend.global.dto.response.SuccessResponse; -import com.webeye.backend.product.dto.request.ProductDetailAnalysisRequest; -import com.webeye.backend.product.dto.response.DetailExplanationResponse; +import com.webeye.backend.productdetail.dto.request.ProductDetailAnalysisRequest; +import com.webeye.backend.productdetail.dto.response.DetailExplanationResponse; import com.webeye.backend.productdetail.domain.type.OutlineType; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; From 9a6522bf2b43970706363945a80085607d893c40 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 19:51:35 +0900 Subject: [PATCH 268/309] =?UTF-8?q?#84=20refactor:=20product=20detail=20co?= =?UTF-8?q?ntent=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../productdetail/application/ProductDetailService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java index 86f575b..110b555 100644 --- a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java +++ b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java @@ -56,6 +56,10 @@ public void saveProductDetails(String id, AllDetailExplanationResponse details) } private ProductDetail createProductDetail(Product product, OutlineType outline, String content) { + if (content == null || content.trim().isEmpty()) { + log.warn("[ProductDetailService] 빈 content가 감지되었습니다. outline: {}, productId: {}", outline, product.getId()); + content = "해당 항목에 대한 정보는 기재되어 있지 않습니다."; + } return ProductDetail.builder() .product(product) .outline(outline) From 8a09c93b72ae945a4091562d5208ffc5ccaf5a71 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Mon, 26 May 2025 19:54:33 +0900 Subject: [PATCH 269/309] =?UTF-8?q?#84=20refactor:=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/product/domain/Product.java | 10 ++++++---- .../presentation/ProductDetailController.java | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/webeye/backend/product/domain/Product.java b/src/main/java/com/webeye/backend/product/domain/Product.java index a39d43f..93e7f07 100644 --- a/src/main/java/com/webeye/backend/product/domain/Product.java +++ b/src/main/java/com/webeye/backend/product/domain/Product.java @@ -76,11 +76,13 @@ public void addHealthFood(ProductHealthfood healthFood) { healthFood.associateWithProduct(this); } + public void addProductDetail(ProductDetail detail) { + this.details.add(detail); + detail.associateWithProduct(this); + } + public void addProductDetails(List details) { - details.forEach(detail -> { - this.details.add(detail); - detail.associateWithProduct(this); - }); + details.forEach(this::addProductDetail); } public void setNutrientReferenceAmount(Integer nutrientReferenceAmount) { diff --git a/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java b/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java index 67926c6..211534f 100644 --- a/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java +++ b/src/main/java/com/webeye/backend/productdetail/presentation/ProductDetailController.java @@ -24,6 +24,7 @@ public class ProductDetailController implements ProductDetailSwagger { @PostMapping(value = "/{outline}") public SuccessResponse productDetailAnalysis( @PathVariable OutlineType outline, @Valid @RequestBody ProductDetailAnalysisRequest request) { - return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, productDetailService.analyzeProductDetail(outline, request)); + return SuccessResponse.of(PRODUCT_DETAIL_EXPLANATION_ANALYSIS_SUCCESS, + productDetailService.analyzeProductDetail(outline, request)); } } From 4b338b0303fed8531e56818ed9b89e8255698f4c Mon Sep 17 00:00:00 2001 From: yeonjy Date: Tue, 27 May 2025 12:34:59 +0900 Subject: [PATCH 270/309] =?UTF-8?q?#84=20feat:=20ProductDetail=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20-=20nullable=20=3D=20false=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/productdetail/domain/ProductDetail.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java b/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java index 228f973..4330f36 100644 --- a/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java +++ b/src/main/java/com/webeye/backend/productdetail/domain/ProductDetail.java @@ -27,10 +27,11 @@ public class ProductDetail extends BaseEntity { @JoinColumn(name = "product_id") private Product product; + @Column(nullable = false) @Enumerated(EnumType.STRING) private OutlineType outline; - @Column(columnDefinition = "TEXT") + @Column(nullable = false, columnDefinition = "TEXT") private String content; @Builder From 212724308c9b7c90fa5db434972cb045cd31b72a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Tue, 27 May 2025 13:57:01 +0900 Subject: [PATCH 271/309] =?UTF-8?q?#86=20fix:=20http://,=20https://,=20//?= =?UTF-8?q?=20=EB=AA=A8=EB=91=90=20=EC=9E=A1=EA=B3=A0=20https=EB=A1=9C=20u?= =?UTF-8?q?rl=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/ImageUrlExtractor.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java index e072401..f9167b0 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -17,16 +17,25 @@ public class ImageUrlExtractor { public List extractImageUrlFromHtml(String html) { html = StringEscapeUtils.unescapeJava(html); - Pattern pattern = Pattern.compile("]+src=[\"'](//[^\"']+)[\"']"); + // http://, https://, // 모두 잡는 정규식 + Pattern pattern = Pattern.compile("]+src=[\"']((https?:)?//[^\"']+)[\"']"); Matcher matcher = pattern.matcher(html); List imageUrls = new ArrayList<>(); while (matcher.find()) { String rawUrl = matcher.group(1); - String fullUrl = "https:" + rawUrl; - imageUrls.add(fullUrl); + + if (rawUrl.startsWith("//")) { + rawUrl = "https:" + rawUrl; + } + else if (rawUrl.startsWith("http://")) { + rawUrl = rawUrl.replaceFirst("http://", "https://"); + } + + imageUrls.add(rawUrl); } + log.info("extracted urls: {}", imageUrls); log.info("total number of images: {}", imageUrls.size()); From 606dd2872f63173b2df070bb1d8d0699a2080c61 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 27 May 2025 15:19:58 +0900 Subject: [PATCH 272/309] =?UTF-8?q?#88=20fix:=20duplicate=20primary=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/product/domain/ProductHealthfood.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java b/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java index 2fc868a..a424a10 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java +++ b/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java @@ -34,7 +34,6 @@ public ProductHealthfood(Product product, HealthFood healthFood) { public void associateWithProduct(Product product) { this.product = product; - product.getHealthFoods().add(this); } public void associateWithHealthFood(HealthFood healthFood) { From ada3f8e4f4f4eec475b80a03bf11b8514daafe91 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 27 May 2025 16:16:07 +0900 Subject: [PATCH 273/309] =?UTF-8?q?#88=20fix:=20duplicate=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webeye/backend/healthfood/application/HealthFoodService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 8f8d39e..4a2cb03 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -90,7 +90,6 @@ public void saveProductHealthFood(Product product, List healthFoods) ProductHealthfood productHealthfood = ProductHealthFoodMapper.toEntity(product, healthFood); product.addHealthFood(productHealthfood); - healthFood.addProduct(productHealthfood); productHealthFoods.add(productHealthfood); } From f4bc242f9addd2c8a03b8e97b8fc1bfb3979a13f Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 27 May 2025 16:55:11 +0900 Subject: [PATCH 274/309] # d --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 0799570..29146d2 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -20,7 +20,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj", "chrome-extension://ghplnadekipfddbmbpcpkophjobfpkkj", "chrome-extension://alnggnpkcolckmkepmkhkdcajipincml", - "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb" + "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb", + "chrome-extension://bhmlpmgeohjakgpfjoddkcbepkhcjhpb" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From bb20ee1391c5b49a71932270c05962dfd5f95a1f Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 27 May 2025 17:56:16 +0900 Subject: [PATCH 275/309] #91: web --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 29146d2..5df2c58 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -21,7 +21,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://ghplnadekipfddbmbpcpkophjobfpkkj", "chrome-extension://alnggnpkcolckmkepmkhkdcajipincml", "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb", - "chrome-extension://bhmlpmgeohjakgpfjoddkcbepkhcjhpb" + "chrome-extension://bhmlpmgeohjakgpfjoddkcbepkhcjhpb", + "chrome-extension://ehiaoibpalckfankempaochcbffhllea" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From fa76795baa5d153d37e12a1871a9e52c0a063b8b Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 28 May 2025 01:27:00 +0900 Subject: [PATCH 276/309] =?UTF-8?q?#88=20fix:=20=EC=97=B0=EA=B4=80?= =?UTF-8?q?=EA=B4=80=EA=B3=84=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B0=8F=20?= =?UTF-8?q?itemNames=20=EB=A7=A4=EC=B9=AD=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/healthfood/application/HealthFoodService.java | 4 +--- .../com/webeye/backend/healthfood/domain/HealthFood.java | 5 ----- .../com/webeye/backend/product/domain/ProductHealthfood.java | 5 ----- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java index 4a2cb03..a71b159 100644 --- a/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java +++ b/src/main/java/com/webeye/backend/healthfood/application/HealthFoodService.java @@ -90,7 +90,6 @@ public void saveProductHealthFood(Product product, List healthFoods) ProductHealthfood productHealthfood = ProductHealthFoodMapper.toEntity(product, healthFood); product.addHealthFood(productHealthfood); - productHealthFoods.add(productHealthfood); } productHealthFoodRepository.saveAll(productHealthFoods); @@ -108,8 +107,7 @@ public Product findOrCreateProduct(String productId) { private List matchItemNames(List aiItemNames, List dbItemNames) { return aiItemNames.stream() - .flatMap(ai -> dbItemNames.stream() - .filter(ai::contains)) + .filter(ai -> dbItemNames.stream().anyMatch(ai::contains)) .distinct() .toList(); } diff --git a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java index d7b47b8..7099ea8 100644 --- a/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java +++ b/src/main/java/com/webeye/backend/healthfood/domain/HealthFood.java @@ -38,9 +38,4 @@ public HealthFood(String itemName, String functionality) { this.itemName = itemName; this.functionality = functionality; } - - public void addProduct(ProductHealthfood healthFood) { - healthfoods.add(healthFood); - healthFood.associateWithHealthFood(this); - } } diff --git a/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java b/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java index a424a10..a09a80d 100644 --- a/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java +++ b/src/main/java/com/webeye/backend/product/domain/ProductHealthfood.java @@ -35,9 +35,4 @@ public ProductHealthfood(Product product, HealthFood healthFood) { public void associateWithProduct(Product product) { this.product = product; } - - public void associateWithHealthFood(HealthFood healthFood) { - this.healthFood = healthFood; - healthFood.getHealthfoods().add(this); - } } From fcbe4d55420f6510e24ce5ccaf05faf536a26ef0 Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 28 May 2025 15:40:11 +0900 Subject: [PATCH 277/309] =?UTF-8?q?#94=20feat:=20review=20request=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ReviewSummaryRequest.java | 32 ++----------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index ec49202..84f2074 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -4,40 +4,14 @@ import lombok.Builder; import java.util.List; -import java.util.Map; @Builder @Schema(description = "리뷰 만족도") public record ReviewSummaryRequest( - @Schema(description = "쿠팡 상품 ID", example = "85241789") + @Schema(description = "쿠팡 제품 ID") String productId, - @Schema(description = "별점 통계") - ReviewRating reviewRating, - - @Schema(description = "리뷰 만족도 통계", example = """ - { - "맛 만족도": { - "맛있어요": 55, - "보통이에요": 16, - "생각보다 별로예요": 29 - }, - "당도": { - "아주 달콤해요": 39, - "적당해요": 29, - "달지 않아요": 32 - } - } - """) - Map> reviews + @Schema(description = "쿠팡 리뷰 목록", example = "[맛있어요, 배송 느려요, 부드러워요]") + List reviews ) { - public record ReviewRating( - @Schema(description = "총 별점 수", example = "150") - int totalCount, - - @Schema(description = "별점 등급별 수", example = """ - [83, 11, 4, 1, 1] - """) - List ratings - ){} } From e7447ebdae4d80a0657b21d0997e8422d77f40ba Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 28 May 2025 18:22:41 +0900 Subject: [PATCH 278/309] =?UTF-8?q?#94=20feat:=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=A4=91=EA=B0=84=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 2 +- .../dto/request/ReviewSummaryRequest.java | 18 +++++++++-- .../clovaX/ClovaXClientService.java | 32 +++++-------------- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 61efce8..7ad8837 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -46,7 +46,7 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { } ReviewSummaryResponse response = clovaXClientService.summarizeReviewText( - request.reviews(), + String.valueOf(request.reviews()), ratingMap, request.reviewRating().totalCount() ); diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 84f2074..75d3f9d 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -8,10 +8,24 @@ @Builder @Schema(description = "리뷰 만족도") public record ReviewSummaryRequest( - @Schema(description = "쿠팡 제품 ID") + @Schema(description = "쿠팡 상품 ID", example = "85241789") String productId, - @Schema(description = "쿠팡 리뷰 목록", example = "[맛있어요, 배송 느려요, 부드러워요]") + @Schema(description = "별점 통계") + ReviewRating reviewRating, + + @Schema(description = "쿠팡 리뷰 목록", example = """ + [맛있어요, 배송 느려요, 부드러워요] + """) List reviews ) { + public record ReviewRating( + @Schema(description = "총 별점 수", example = "150") + int totalCount, + + @Schema(description = "별점 등급별 수", example = """ + [83, 11, 4, 1, 1] + """) + List ratings + ){} } diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 12db849..03dbb9c 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -28,14 +28,16 @@ public class ClovaXClientService { @Value("${clova.request-id}") private String requestId; - public ReviewSummaryResponse summarizeReviewText(Map> reviewText, Map ratingMap, int totalCount) { + public ReviewSummaryResponse summarizeReviewText(String reviewText, Map ratingMap, int totalCount) { double averageRating = ReviewCalculator.calculateAverageRating(ratingMap, totalCount); - String inputText = convertReviewMapToText(reviewText); - ClovaXRequest request = buildReviewSummaryPrompt(inputText); + ClovaXRequest request = buildReviewSummaryPrompt(reviewText); ClovaXResponse clovaXResponse = clovaXClient.createReviewSummary("Bearer "+ secretKey, requestId, request); + String content = clovaXResponse.result().message().content(); + log.info("[Clova 요약 응답] content = {}", content); + return parseResponse(clovaXResponse.result().message().content(), totalCount, averageRating); } @@ -55,12 +57,6 @@ private ClovaXRequest buildReviewSummaryPrompt(String inputText) { 긍정 리뷰: 맛있다는 평가가 많습니다, 달콤하다는 평가가 많습니다 부정 리뷰: 배송이 느리다는 평가가 많습니다, 포장이 별로라는 평가가 많습니다 키워드: 맛있어요, 신선해요, 배송이 느려요 - - 주의: - 1. 긍정 리뷰와 부정 리뷰는 최대 3개까지 표시할 것 - 2. 제공된 입력 내용을 기반으로만 판단하고, 임의로 생성하지 말 것 - 3. 각 항목에서 수치가 가장 높은 선택지만 참고할 것 - 4. 만족도를 키워드로 추출할 경우, 만족도의 긍부정 여부도 함께 표시할 것 (예시 - 만족도 높음) """; return new ClovaXRequest(List.of( @@ -73,7 +69,6 @@ private ClovaXRequest buildReviewSummaryPrompt(String inputText) { )); } - private ReviewSummaryResponse parseResponse(String content, int totalCount, double averageRating) { String[] lines = content.split("\n"); @@ -81,7 +76,9 @@ private ReviewSummaryResponse parseResponse(String content, int totalCount, doub List negativeReviews = new ArrayList<>(); List keywords = new ArrayList<>(); - for (String line : lines) { + for (String rawLine : lines) { + String line = rawLine.replaceAll("\\*\\*", "").trim(); + if (line != null && line.startsWith("긍정 리뷰:")) { String text = line.replace("긍정 리뷰:", "").trim(); positiveReviews = Arrays.stream(text.split("[.,]")) @@ -107,17 +104,4 @@ private ReviewSummaryResponse parseResponse(String content, int totalCount, doub } return new ReviewSummaryResponse(totalCount, averageRating, positiveReviews, negativeReviews, keywords); } - - private String convertReviewMapToText(Map> reviewMap) { - StringBuilder sb = new StringBuilder(); - - for (Map.Entry> entry : reviewMap.entrySet()) { - sb.append("[").append(entry.getKey()).append("]\n"); - for (Map.Entry option : entry.getValue().entrySet()) { - sb.append("- ").append(option.getKey()).append(": ").append(option.getValue()).append("%\n"); - } - sb.append("\n"); - } - return sb.toString(); - } } From 02f8ee889bcf612a23a8bef1f56bd7c237fd7e9e Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 28 May 2025 18:27:10 +0900 Subject: [PATCH 279/309] #95 refactor: cors cnrk --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 5df2c58..2e40be6 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -22,7 +22,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://alnggnpkcolckmkepmkhkdcajipincml", "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb", "chrome-extension://bhmlpmgeohjakgpfjoddkcbepkhcjhpb", - "chrome-extension://ehiaoibpalckfankempaochcbffhllea" + "chrome-extension://ehiaoibpalckfankempaochcbffhllea", + "chrome-extension://chjaphejcjooaehbhlhklkmjkcfjdomc" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 92a484c6e6cb42e776bceaf796e1ea047714ab5c Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 28 May 2025 21:51:12 +0900 Subject: [PATCH 280/309] =?UTF-8?q?#94=20fix:=20prompt=20=EB=B0=8F=20parse?= =?UTF-8?q?Response=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clovaX/ClovaXClientService.java | 28 ++++++++++++------- .../review/presentation/ReviewController.java | 7 ----- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 03dbb9c..344b245 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -35,8 +35,7 @@ public ReviewSummaryResponse summarizeReviewText(String reviewText, Map !s.isBlank()) .limit(3) .toList(); - } else if (line != null && line.startsWith("부정 리뷰:")) { + } else if (line.startsWith("부정 리뷰:")) { String text = line.replace("부정 리뷰:", "").trim(); - negativeReviews = Arrays.stream(text.split("[.,]")) + + negativeReviews = Arrays.stream(text.split("[.]")) .map(String::trim) .filter(s -> !s.isBlank()) .limit(3) .toList(); - } else if (line != null && line.startsWith("키워드:")) { + } else if (line.startsWith("키워드:")) { String[] tokens = line.replace("키워드:", "").split(","); + keywords = Arrays.stream(tokens) .map(String::trim) .filter(s -> !s.isBlank()) diff --git a/src/main/java/com/webeye/backend/review/presentation/ReviewController.java b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java index 3e56dd0..25ee0ef 100644 --- a/src/main/java/com/webeye/backend/review/presentation/ReviewController.java +++ b/src/main/java/com/webeye/backend/review/presentation/ReviewController.java @@ -25,13 +25,6 @@ public class ReviewController implements ReviewSwagger { @PostMapping("/summary") @ResponseStatus(HttpStatus.OK) public SuccessResponse summarizeReview(@RequestBody @Valid ReviewSummaryRequest request) { - log.info("[리뷰 요약 요청] productId={}, totalCount={}, ratings={}, reviews={}", - request.productId(), - request.reviewRating().totalCount(), - request.reviewRating().ratings(), - request.reviews() - ); - return SuccessResponse.of(REVIEW_SUMMARY_SUCCESS, reviewService.summarizeReview(request)); } } From 367a3a6c150f12ea73e42a4761575f356246c0ee Mon Sep 17 00:00:00 2001 From: zyovn Date: Wed, 28 May 2025 21:54:36 +0900 Subject: [PATCH 281/309] =?UTF-8?q?#97=20refactor:=20webConfig=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 2e40be6..c58e3d1 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -23,7 +23,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb", "chrome-extension://bhmlpmgeohjakgpfjoddkcbepkhcjhpb", "chrome-extension://ehiaoibpalckfankempaochcbffhllea", - "chrome-extension://chjaphejcjooaehbhlhklkmjkcfjdomc" + "chrome-extension://chjaphejcjooaehbhlhklkmjkcfjdomc", + "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From afe688098953afef8cd4ad18702bd0f14b50e7ad Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 00:49:48 +0900 Subject: [PATCH 282/309] =?UTF-8?q?#94=20refactor:=20schema=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/review/dto/request/ReviewSummaryRequest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java index 75d3f9d..320d62f 100644 --- a/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java +++ b/src/main/java/com/webeye/backend/review/dto/request/ReviewSummaryRequest.java @@ -14,9 +14,7 @@ public record ReviewSummaryRequest( @Schema(description = "별점 통계") ReviewRating reviewRating, - @Schema(description = "쿠팡 리뷰 목록", example = """ - [맛있어요, 배송 느려요, 부드러워요] - """) + @Schema(description = "쿠팡 리뷰 목록", example = "[\"맛있어요\", \"배송 느려요\", \"부드러워요\"]") List reviews ) { public record ReviewRating( From 0313f02713363cbc9d2e9388fe748fe26c907474 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 01:06:23 +0900 Subject: [PATCH 283/309] =?UTF-8?q?#94=20fix:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/review/application/ReviewService.java | 2 +- .../review/infrastructure/clovaX/ClovaXClientService.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/application/ReviewService.java b/src/main/java/com/webeye/backend/review/application/ReviewService.java index 7ad8837..c959f6b 100644 --- a/src/main/java/com/webeye/backend/review/application/ReviewService.java +++ b/src/main/java/com/webeye/backend/review/application/ReviewService.java @@ -46,7 +46,7 @@ public ReviewSummaryResponse summarizeReview(ReviewSummaryRequest request) { } ReviewSummaryResponse response = clovaXClientService.summarizeReviewText( - String.valueOf(request.reviews()), + String.join(". ", request.reviews()), ratingMap, request.reviewRating().totalCount() ); diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index 344b245..ae4ab4f 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -46,8 +46,8 @@ private ClovaXRequest buildReviewSummaryPrompt(String inputText) { **번호 및 글머리 기호를 붙이지 말고**, 정확한 형식을 지켜줘. '~며', '~고', '~하며' 같은 연결어도 절대 쓰지 마. 문장이 길어지지 않게 최대한 간결하게 써. - 1. 긍정적인 내용 요약 (한 문장으로, 콤마로 구분, 서로 독립된 짧은 문장으로 작성) - 2. 부정적인 내용 요약 (한 문장으로, 콤마로 구분, 서로 독립된 짧은 문장으로 작성) + 1. 긍정적인 내용 요약 (한 문장으로, 마침표로 구분, 서로 독립된 짧은 문장으로 작성) + 2. 부정적인 내용 요약 (한 문장으로, 마침표로 구분, 서로 독립된 짧은 문장으로 작성) 3. 대표 키워드 3개 추출 (한 문장으로, 콤마로 구분) 결과는 반드시 다음 형식으로 반환해: From 68b18fbfa765dd4a3ad5dc3f9a89b7c43efc4a2a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 02:15:49 +0900 Subject: [PATCH 284/309] =?UTF-8?q?#99=20refactor:=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=84=A4=EB=AA=85=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../imageanalysis/infrastructure/OpenAiClient.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 461fc30..7e1309b 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -41,18 +41,19 @@ public AllDetailExplanationResponse explainProductAllDetail(List urls) { String system = """ You are an expert in providing detailed explanations about products based on images. When a user provides a product description image along with the key outline of that description, you should offer a clear and detailed explanation of that element. - In this explanation, you must provide very detailed information about that element from the image. Answer in Korean. - I'd like it to have a descriptive tone, like "부셔서 먹는 방식으로, 남녀노소 즐길 수 있는 간식입니다." + In this explanation, you must provide very detailed information about that element from the image. Answer in Korean. + The output must use a descriptive tone ending in "~입니다." or "~합니다." Only declarative sentences are allowed. Do not use imperative, interrogative, or any other sentence endings. + Insert a \n between each sentence in the response, and add a dash (-) in front of each sentence to format it as a list. """; String user = String.format(""" Please generate a detailed explanation of the provided key. Provide your answer following the FORMAT I provided. I will specify what content should be included for each key. + In your response, if there are any words that contain important information, please wrap them with the HTML tag. main: %s usage: %s warning: %s specs: %s - certification: %s - """, OutlineType.MAIN.getPrompt(), OutlineType.USAGE.getPrompt(), OutlineType.WARNING.getPrompt(), OutlineType.SPECS.getPrompt(), OutlineType.CERTIFICATION.getPrompt()); + """, OutlineType.MAIN.getPrompt(), OutlineType.USAGE.getPrompt(), OutlineType.WARNING.getPrompt(), OutlineType.SPECS.getPrompt()); ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); return callWithStructuredOutput(urls, prompt, AllDetailExplanationResponse.class); @@ -60,8 +61,7 @@ public AllDetailExplanationResponse explainProductAllDetail(List urls) { public AllergyAiResponse explainAllergy(List urls) { String system = """ - You are an OCR assistant that extracts and detects allergenic ingredients from Korean product label images. Always treat partial matches inside compound words as valid if they contain the full Korean name of an allergen. - + You are an OCR assistant that extracts and detects allergenic ingredients from Korean product label images. Always treat partial matches inside compound words as valid if they contain the full Korean name of an allergen. """; String user = """ From 917fb3cce0d87fa9d3f4cc8130cc26a60ee9ce6a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 02:16:13 +0900 Subject: [PATCH 285/309] =?UTF-8?q?#99=20refactor:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=ED=83=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../productdetail/application/ProductDetailService.java | 4 +--- .../backend/productdetail/domain/type/OutlineType.java | 5 ++--- .../dto/response/AllDetailExplanationResponse.java | 3 +-- .../presentation/swagger/ProductDetailSwagger.java | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java index 110b555..b50cd2a 100644 --- a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java +++ b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java @@ -48,8 +48,7 @@ public void saveProductDetails(String id, AllDetailExplanationResponse details) createProductDetail(product, OutlineType.MAIN, details.main()), createProductDetail(product, OutlineType.USAGE, details.usage()), createProductDetail(product, OutlineType.WARNING, details.warning()), - createProductDetail(product, OutlineType.SPECS, details.specs()), - createProductDetail(product, OutlineType.CERTIFICATION, details.certification()) + createProductDetail(product, OutlineType.SPECS, details.specs()) ); product.addProductDetails(productDetails); productDetailRepository.saveAll(productDetails); @@ -82,7 +81,6 @@ private String getContentByOutline(AllDetailExplanationResponse details, Outline case USAGE -> details.usage(); case WARNING -> details.warning(); case SPECS -> details.specs(); - case CERTIFICATION -> details.certification(); }; } } diff --git a/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java b/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java index baecf9a..e167d0b 100644 --- a/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java +++ b/src/main/java/com/webeye/backend/productdetail/domain/type/OutlineType.java @@ -4,11 +4,10 @@ @Getter public enum OutlineType { - MAIN("Analyze the product description image I send and provide information about the main ingredients, materials, functions, and components of the product. If it is a food item, nutritional information is required. (e.g., ingredients/nutritional content, material, functionality, components, country of origin, manufacturer)"), + MAIN("Analyze the product description image I send and provide information about the main ingredients, materials, functions, and components of the product. If it is a food item, nutritional information and Manufacturer and Distributor(Business Name and Address) must be included without exception. (e.g., ingredients/nutritional content, country of origin, material, functionality, components, manufacturer)"), USAGE("Analyze the product description image I send and provide information about how to use (consume/assemble/install/utilize) the product and the intended user. (e.g., consumption method, usage steps, recommended users, age group, installation method)"), WARNING("Analyze the product description image I send and provide information about storage methods, expiration date, safety precautions, allergies, cleaning, etc. If it is a food item, allergy information is required. (e.g., storage method, avoid direct sunlight, expiration date, washing, prohibitions)"), - SPECS("Analyze the product description image I send and provide information about the product’s color, size, weight, capacity, compatibility, and purchasing options. (e.g., size, weight, capacity, size options, color, option configuration, coverage)"), - CERTIFICATION("Analyze the product description image I send and provide information about the product’s certifications, after-sales service, packaging, brand, delivery, and marks. (e.g., KC certification, GMP, vegan, KFDA, free A/S, packaging)") + SPECS("Analyze the product description image I send and provide information about the product’s color, size, weight, capacity, compatibility, and purchasing options. (e.g., size, weight, capacity, size options, color, option configuration, coverage)") ; private final String prompt; diff --git a/src/main/java/com/webeye/backend/productdetail/dto/response/AllDetailExplanationResponse.java b/src/main/java/com/webeye/backend/productdetail/dto/response/AllDetailExplanationResponse.java index 0b5b5b7..642c615 100644 --- a/src/main/java/com/webeye/backend/productdetail/dto/response/AllDetailExplanationResponse.java +++ b/src/main/java/com/webeye/backend/productdetail/dto/response/AllDetailExplanationResponse.java @@ -7,7 +7,6 @@ public record AllDetailExplanationResponse( String main, String usage, String warning, - String specs, - String certification + String specs ) { } diff --git a/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java b/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java index 57310a0..e0cfd6a 100644 --- a/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java +++ b/src/main/java/com/webeye/backend/productdetail/presentation/swagger/ProductDetailSwagger.java @@ -17,7 +17,7 @@ public interface ProductDetailSwagger { @Operation( summary = "제품 주요 개요에 대한 상세 설명 추출", description = "제품 ID와 상세 설명 이미지가 포함된 HTML, 개요를 입력받아 주요 요소에 대한 상세 설명을 추출합니다. " + - "MAIN: 주요정보, USAGE: 사용정보, WARNING: 주의 및 보관, SPECS: 규격 및 옵션, CERTIFICATION: 인증 및 기타" + "MAIN: 주요정보, USAGE: 사용정보, WARNING: 주의 및 보관, SPECS: 규격 및 옵션" ) @ApiResponses(value = { @ApiResponse( From c63ed5e3101eaa7b1bb062538862ee7243803445 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 10:17:44 +0900 Subject: [PATCH 286/309] =?UTF-8?q?#102=20refactor:=20allowed=20origins=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 --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index c58e3d1..1c08031 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -24,7 +24,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://bhmlpmgeohjakgpfjoddkcbepkhcjhpb", "chrome-extension://ehiaoibpalckfankempaochcbffhllea", "chrome-extension://chjaphejcjooaehbhlhklkmjkcfjdomc", - "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh" + "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh", + "chrome-extension://lhgneonbbjkppefiifpanchnbaigecil" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 0866d1ec5c9267dab439c4069cbcaf269473cdfb Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 11:45:37 +0900 Subject: [PATCH 287/309] #104 refactor: add review log --- .../infrastructure/clovaX/ClovaXClientService.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java index ae4ab4f..dae42d3 100644 --- a/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java +++ b/src/main/java/com/webeye/backend/review/infrastructure/clovaX/ClovaXClientService.java @@ -33,11 +33,17 @@ public ReviewSummaryResponse summarizeReviewText(String reviewText, Map Date: Thu, 29 May 2025 12:46:00 +0900 Subject: [PATCH 288/309] #106 fix: deploy test 1 --- .github/workflows/deploy.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c8de8a4..7be695f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI/CD Pipeline on: push: - branches: [ "main", "develop" ] + branches: [ "main", "develop", "fix/#106-deploy" ] jobs: build-docker-image: @@ -25,6 +25,17 @@ jobs: - name: Add execute permission for Gradle Wrapper run: chmod +x ./gradlew + # Gradle 캐시 설정 + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: "${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" + restore-keys: | + ${{ runner.os }}-gradle- + - name: Ensure resources directory exists run: mkdir -p src/main/resources From c3b8277698e12347b47edfa41eea699ae26319e3 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 13:04:50 +0900 Subject: [PATCH 289/309] #106 fix: deploy tesy 2 --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7be695f..371b128 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -71,6 +71,7 @@ jobs: - name: Push Docker Image run: docker push ${{ secrets.DOCKER_USERNAME }}/webeye-server:latest + deploy-pipeline: needs: build-docker-image runs-on: ubuntu-latest From 7c4523902527e2621d34a7c88446cae22d303c8e Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 13:42:11 +0900 Subject: [PATCH 290/309] =?UTF-8?q?#107=20refactor:=20=EC=9D=8C=EC=8B=9D?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EB=B6=84=EC=84=9D=20-=20=EA=B0=81=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=A5=20=EC=95=9E=20=EA=B8=80=EB=A8=B8=EB=A6=AC?= =?UTF-8?q?=EA=B8=B0=ED=98=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 7e1309b..361a220 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -43,7 +43,7 @@ public AllDetailExplanationResponse explainProductAllDetail(List urls) { When a user provides a product description image along with the key outline of that description, you should offer a clear and detailed explanation of that element. In this explanation, you must provide very detailed information about that element from the image. Answer in Korean. The output must use a descriptive tone ending in "~입니다." or "~합니다." Only declarative sentences are allowed. Do not use imperative, interrogative, or any other sentence endings. - Insert a \n between each sentence in the response, and add a dash (-) in front of each sentence to format it as a list. + Please separate each sentence onto a new line using \\n as the line break. """; String user = String.format(""" Please generate a detailed explanation of the provided key. From c124f5130938f289ca9e3a612061122fe45da696 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 13:42:38 +0900 Subject: [PATCH 291/309] #106 fix: deploy test 3 --- .github/workflows/deploy.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 371b128..7be695f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -71,7 +71,6 @@ jobs: - name: Push Docker Image run: docker push ${{ secrets.DOCKER_USERNAME }}/webeye-server:latest - deploy-pipeline: needs: build-docker-image runs-on: ubuntu-latest From bcbe7c9c19a6dbf539aff87a71c673d7c9209976 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 14:19:52 +0900 Subject: [PATCH 292/309] =?UTF-8?q?#109=20refactor:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EA=B0=9C=EC=88=98=20=2020=EA=B0=9C=20=EC=B4=88?= =?UTF-8?q?=EA=B3=BC=ED=95=A0=20=EA=B2=BD=EC=9A=B0=2020=EA=B0=9C=EB=A1=9C?= =?UTF-8?q?=20=EC=A0=9C=ED=95=9C=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/ImageUrlExtractor.java | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java index a2581c3..f1cfd6e 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -12,10 +12,28 @@ @Slf4j public class ImageUrlExtractor { + private static final int MAX_IMAGE_COUNT = 20; + public static List extractImageUrlFromHtml(String html) { - html = StringEscapeUtils.unescapeJava(html); + String unescapedHtml = unescapeHtml(html); + List imageUrls = extractImageUrls(unescapedHtml); + List trimmedUrls = trimToMaxSize(imageUrls, MAX_IMAGE_COUNT); + + log.info("extracted urls: {}", trimmedUrls); + log.info("total number of images: {}", trimmedUrls.size()); + + if (trimmedUrls.isEmpty()) { + throw new BusinessException(ErrorCode.IMAGE_URL_NOT_FOUND); + } + + return trimmedUrls; + } - // http://, https://, // 모두 잡는 정규식 + private static String unescapeHtml(String html) { + return StringEscapeUtils.unescapeJava(html); + } + + private static List extractImageUrls(String html) { Pattern pattern = Pattern.compile("]+src=[\"']((https?:)?//[^\"']+)[\"']"); Matcher matcher = pattern.matcher(html); @@ -26,20 +44,20 @@ public static List extractImageUrlFromHtml(String html) { if (rawUrl.startsWith("//")) { rawUrl = "https:" + rawUrl; - } - else if (rawUrl.startsWith("http://")) { + } else if (rawUrl.startsWith("http://")) { rawUrl = rawUrl.replaceFirst("http://", "https://"); } imageUrls.add(rawUrl); } - log.info("extracted urls: {}", imageUrls); - log.info("total number of images: {}", imageUrls.size()); + return imageUrls; + } - if (imageUrls.isEmpty()) { - throw new BusinessException(ErrorCode.IMAGE_URL_NOT_FOUND); + private static List trimToMaxSize(List urls, int maxSize) { + if (urls.size() <= maxSize) { + return urls; } - return imageUrls; + return new ArrayList<>(urls.subList(urls.size() - maxSize, urls.size())); } -} +} \ No newline at end of file From 9bba2f85b2b20f582df1f2fafd5c1d162966a165 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 14:19:53 +0900 Subject: [PATCH 293/309] =?UTF-8?q?#106=20fix:=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7be695f..8cde663 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI/CD Pipeline on: push: - branches: [ "main", "develop", "fix/#106-deploy" ] + branches: [ "main", "develop" ] jobs: build-docker-image: From 8fd2cdbf9c1e1766ca348105ba4046948befa4d9 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 14:20:24 +0900 Subject: [PATCH 294/309] =?UTF-8?q?#109=20refactor:=20Log=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/product/application/ProductService.java | 1 + .../backend/productdetail/application/ProductDetailService.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/java/com/webeye/backend/product/application/ProductService.java b/src/main/java/com/webeye/backend/product/application/ProductService.java index 9ee9f3e..0b52f58 100644 --- a/src/main/java/com/webeye/backend/product/application/ProductService.java +++ b/src/main/java/com/webeye/backend/product/application/ProductService.java @@ -34,6 +34,7 @@ public class ProductService { @Transactional public ProductResponse analyzeFoodProduct(FoodProductAnalysisRequest request) { + log.info("[ProductService] 📌 requested product ID: {}", request.productId()); if (productRepository.existsById(request.productId())) { Product product = productRepository.findById(request.productId()) .orElseThrow(() -> new BusinessException(ErrorCode.PRODUCT_NOT_FOUND)); diff --git a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java index b50cd2a..f218195 100644 --- a/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java +++ b/src/main/java/com/webeye/backend/productdetail/application/ProductDetailService.java @@ -29,6 +29,7 @@ public class ProductDetailService { @Transactional public DetailExplanationResponse analyzeProductDetail(OutlineType outline, ProductDetailAnalysisRequest request) { + log.info("[ProductDetailService] 📌 requested product ID: {}", request.productId()); Optional productDetailOpt = productDetailRepository.findByProductIdAndOutline(request.productId(), outline); if (productDetailOpt.isEmpty()) { AllDetailExplanationResponse details = openAiClient.explainProductAllDetail(ImageUrlExtractor.extractImageUrlFromHtml(request.html())); From 5dc0516cb0ae2e403933aeeabaeccc605da21212 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 14:51:40 +0900 Subject: [PATCH 295/309] =?UTF-8?q?#112=20fix:=20OpenAI=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EA=B0=9C=EC=88=98=20?= =?UTF-8?q?15=EA=B0=9C=EB=A1=9C=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/ImageUrlExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java index f1cfd6e..395ca0d 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -12,7 +12,7 @@ @Slf4j public class ImageUrlExtractor { - private static final int MAX_IMAGE_COUNT = 20; + private static final int MAX_IMAGE_COUNT = 15; public static List extractImageUrlFromHtml(String html) { String unescapedHtml = unescapeHtml(html); From 8b9350e3d643512ebaf113cdd7559e0a4e597236 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 15:14:57 +0900 Subject: [PATCH 296/309] =?UTF-8?q?#112=20fix:=20openai=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/ImageUrlExtractor.java | 2 +- .../imageanalysis/infrastructure/OpenAiClient.java | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java index 395ca0d..3db3be9 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -12,7 +12,7 @@ @Slf4j public class ImageUrlExtractor { - private static final int MAX_IMAGE_COUNT = 15; + private static final int MAX_IMAGE_COUNT = 10; public static List extractImageUrlFromHtml(String html) { String unescapedHtml = unescapeHtml(html); diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index 361a220..f0af35a 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -66,20 +66,20 @@ public AllergyAiResponse explainAllergy(List urls) { String user = """ Step 1: Carefully examine the attached image(s). - + Identify the box that contains the full list of ingredients, labeled with '원재료명' or similar (e.g., '원재료 및 함량'). - Only extract text from this box — ignore all other parts of the image, including allergy warnings or separate notices. - + If there is no match or anything similar, skip to Step 4 and set the value of all keys to false. + Step 2: From the identified box, extract the ingredient list **exactly as written**, without summarizing, translating, or omitting anything — including words like '분말', '가루', or '함유'. - + Step 3: Compare the text to the following list of allergenic ingredients: 계란(EGG), 우유(MILK), 메밀(BUCKWHEAT), 땅콩(PEANUT), 대두(SOYBEAN), 밀(WHEAT), 잣(PINE_NUT), 호두(WALNUT), 게(CRAB), 새우(SHRIMP), 오징어(SQUID), 고등어(MACKEREL), 조개(SHELLFISH), 복숭아(PEACH), 토마토(TOMATO), 닭고기(CHICKEN), 돼지고기(PORK), 쇠고기(BEEF), 아황산류(SULFITE). Return true for an allergen if its full Korean name appears **anywhere inside any word** in the ingredient list — even if it is part of a compound word (e.g., "호두함유", "호두분말", "밀가루"). - + Return false if the full Korean allergen word does not appear in any part of the ingredient text. - + Step 4: Output a list of booleans (true/false), in the same order as the allergen list above. """; From 2eba3889567e8a0a141fedbb1487a458e052f0fd Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 15:30:36 +0900 Subject: [PATCH 297/309] =?UTF-8?q?#116=20fix:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=95=9C=EB=8F=84=20=EA=B0=9C=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/ImageUrlExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java index 3db3be9..4cfeccb 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -12,7 +12,7 @@ @Slf4j public class ImageUrlExtractor { - private static final int MAX_IMAGE_COUNT = 10; + private static final int MAX_IMAGE_COUNT = 6; public static List extractImageUrlFromHtml(String html) { String unescapedHtml = unescapeHtml(html); From 379ef57a8cf80b673a8b0120fb40e11473a4fc48 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 15:49:37 +0900 Subject: [PATCH 298/309] =?UTF-8?q?#116=20refactor:=20image=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/ImageUrlExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java index 4cfeccb..ca994fc 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/ImageUrlExtractor.java @@ -12,7 +12,7 @@ @Slf4j public class ImageUrlExtractor { - private static final int MAX_IMAGE_COUNT = 6; + private static final int MAX_IMAGE_COUNT = 17; public static List extractImageUrlFromHtml(String html) { String unescapedHtml = unescapeHtml(html); From 4b7439fa0d3a20f99c89879f3570ca8964558119 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 20:32:01 +0900 Subject: [PATCH 299/309] =?UTF-8?q?#119=20refactor:=20cors=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/webeye/backend/global/config/WebConfig.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 1c08031..29b65ef 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -16,16 +16,9 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm", "chrome-extension://jeppkpjgeheckphiogogbffdenhlkclh", "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf", - "chrome-extension://adkanhcmkdbilgdibndapnlhkcnfoglg", - "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj", - "chrome-extension://ghplnadekipfddbmbpcpkophjobfpkkj", - "chrome-extension://alnggnpkcolckmkepmkhkdcajipincml", - "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb", - "chrome-extension://bhmlpmgeohjakgpfjoddkcbepkhcjhpb", - "chrome-extension://ehiaoibpalckfankempaochcbffhllea", - "chrome-extension://chjaphejcjooaehbhlhklkmjkcfjdomc", - "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh", - "chrome-extension://lhgneonbbjkppefiifpanchnbaigecil" + "chrome-extension://lhgneonbbjkppefiifpanchnbaigecil", + "chrome-extension://mphnlcljehhgppcoamgpnnaamidpjkch", + "chrome-extension://licieibgjjilbdjfheljdmllhbopbajo" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From a7312638af43dfcf5ecacedbd1b8077cfc24be1b Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 21:05:55 +0900 Subject: [PATCH 300/309] =?UTF-8?q?#121=20refactor:=20cors=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 29b65ef..05cb572 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -18,7 +18,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://ecbaebehchfclbcglpabiclbgkjoihmf", "chrome-extension://lhgneonbbjkppefiifpanchnbaigecil", "chrome-extension://mphnlcljehhgppcoamgpnnaamidpjkch", - "chrome-extension://licieibgjjilbdjfheljdmllhbopbajo" + "chrome-extension://licieibgjjilbdjfheljdmllhbopbajo", + "chrome-extension://jbnhfmonamkklnglmdhklbfkojofhppk" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 1bf2ee73786877726f6ac3192ebe2cdac02becc1 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 21:13:04 +0900 Subject: [PATCH 301/309] #123 feat: allowed origin --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 05cb572..1d276bb 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -19,7 +19,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://lhgneonbbjkppefiifpanchnbaigecil", "chrome-extension://mphnlcljehhgppcoamgpnnaamidpjkch", "chrome-extension://licieibgjjilbdjfheljdmllhbopbajo", - "chrome-extension://jbnhfmonamkklnglmdhklbfkojofhppk" + "chrome-extension://jbnhfmonamkklnglmdhklbfkojofhppk", + "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From fafe5687a1a3fe989a611e99dd0af23d236ed70d Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 22:24:16 +0900 Subject: [PATCH 302/309] =?UTF-8?q?#125=20fix:=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 1d276bb..13bcaea 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -10,7 +10,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins( + .allowedOriginPatterns( "https://voim.store", "http://localhost:3000", "chrome-extension://iofbhhcbidmfcmpjndglaignlfdojpcm", From 507d7b143ac1759f570fe63396cd5f0683dc0a1a Mon Sep 17 00:00:00 2001 From: yeonjy Date: Thu, 29 May 2025 22:47:42 +0900 Subject: [PATCH 303/309] =?UTF-8?q?#127=20refactor:=20=EC=8B=9D=ED=92=88?= =?UTF-8?q?=20=EC=A0=9C=EB=AA=A9=20=ED=95=84=EC=88=98=EC=A0=81=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/product/dto/request/FoodProductAnalysisRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java index cbbcd1f..e2087f6 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java @@ -17,7 +17,6 @@ public record FoodProductAnalysisRequest( @NotEmpty(message = "제품 ID는 비어있을 수 없습니다.") String productId, - @NotBlank(message = "상품 제목은 비어있을 수 없습니다.") @Schema(description = "상품 제목") String title, From 5c4ff4f026798a407f5996071506496fc59a1148 Mon Sep 17 00:00:00 2001 From: zyovn Date: Thu, 29 May 2025 22:49:46 +0900 Subject: [PATCH 304/309] =?UTF-8?q?#128=20refactor:=20cors=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 13bcaea..eaf460e 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -20,7 +20,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://mphnlcljehhgppcoamgpnnaamidpjkch", "chrome-extension://licieibgjjilbdjfheljdmllhbopbajo", "chrome-extension://jbnhfmonamkklnglmdhklbfkojofhppk", - "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb" + "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb", + "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 4e743c5d6b48e4b4eee58bec034024e2ddae984d Mon Sep 17 00:00:00 2001 From: zyovn Date: Fri, 30 May 2025 16:28:52 +0900 Subject: [PATCH 305/309] =?UTF-8?q?#131=20refactor:=20cors=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index eaf460e..5cc9eec 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -21,7 +21,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://licieibgjjilbdjfheljdmllhbopbajo", "chrome-extension://jbnhfmonamkklnglmdhklbfkojofhppk", "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb", - "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj" + "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj", + "chrome-extension://gdainjmbpilaoiinccfnbfgofpchibkl" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 7be2f76397b66385f9213f542faba20dbf236266 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 31 May 2025 01:11:36 +0900 Subject: [PATCH 306/309] =?UTF-8?q?#134=20fix:=20=ED=95=9C=EA=B5=AD?= =?UTF-8?q?=EC=96=B4=20=EC=9D=91=EB=8B=B5=20=EA=B0=95=EC=A1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/imageanalysis/infrastructure/OpenAiClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java index f0af35a..3d2eb1c 100644 --- a/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java +++ b/src/main/java/com/webeye/backend/imageanalysis/infrastructure/OpenAiClient.java @@ -195,10 +195,10 @@ public ImageAnalysisResponse explainImage(ImageAnalysisRequest request) { You are a helpful and concise assistant that specializes in describing images. Always provide clear, accurate, and human-like descriptions of the image content. Focus on the most important visual details such as objects, people, actions, scenes, and context. - Avoid speculation unless necessary, and do not include irrelevant information. Answer in Korean + Avoid speculation unless necessary, and do not include irrelevant information. Answer in Korean. """; String user = """ - Please describe the contents of this image in detail. + Please describe the contents of this image in detail. You must respond in Korean. """; ImageAnalysisPrompt prompt = new ImageAnalysisPrompt(system, user); From 6aa5efa3e277a1fce6560975e980923446ae58f1 Mon Sep 17 00:00:00 2001 From: yeonjy Date: Sat, 31 May 2025 01:12:00 +0900 Subject: [PATCH 307/309] =?UTF-8?q?#134=20refactor:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=EB=AC=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/product/dto/request/FoodProductAnalysisRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java index e2087f6..cabd507 100644 --- a/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java +++ b/src/main/java/com/webeye/backend/product/dto/request/FoodProductAnalysisRequest.java @@ -3,7 +3,6 @@ import com.webeye.backend.allergy.type.AllergyType; import com.webeye.backend.nutrition.domain.type.Gender; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; From 866404ec97251503cd9b2cc629d8b02d1bd789b1 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 31 May 2025 14:00:45 +0900 Subject: [PATCH 308/309] =?UTF-8?q?#133=20refactor:=20cors=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 5cc9eec..4ee3c29 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -22,7 +22,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://jbnhfmonamkklnglmdhklbfkojofhppk", "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb", "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj", - "chrome-extension://gdainjmbpilaoiinccfnbfgofpchibkl" + "chrome-extension://lfcaogbnmpkdghiabipdbhbedhinbfdk", + "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") From 71b03f5f966f45c693d9f303c7b460f5760dc6f2 Mon Sep 17 00:00:00 2001 From: zyovn Date: Sat, 31 May 2025 15:07:13 +0900 Subject: [PATCH 309/309] =?UTF-8?q?#139=20refactor:=20cors=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/webeye/backend/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/webeye/backend/global/config/WebConfig.java b/src/main/java/com/webeye/backend/global/config/WebConfig.java index 4ee3c29..81808f1 100644 --- a/src/main/java/com/webeye/backend/global/config/WebConfig.java +++ b/src/main/java/com/webeye/backend/global/config/WebConfig.java @@ -23,7 +23,8 @@ public void addCorsMappings(CorsRegistry registry) { "chrome-extension://ehgaglekgllijnoglmdfeingpecfjbmb", "chrome-extension://libhakfegdlojphbiaaejoopedaodbgj", "chrome-extension://lfcaogbnmpkdghiabipdbhbedhinbfdk", - "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh" + "chrome-extension://hemleepcpkkmkapaliflaohhnnapfdlh", + "chrome-extension://pneljnbmcceppnnphbeljojgmkooblpn" ) .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*")