From 641988b0335179c6110bab82cceac1490ae98d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chyeonggkim=E2=80=9D?= <“hyeonggkim@smilegate.com”> Date: Mon, 27 Oct 2025 16:06:32 +0900 Subject: [PATCH 1/8] refactor: update translation API and improve footer component logic --- i18n/locales/ko.ts | 11 ++-- layers/components/layouts/Footer.vue | 86 +++++++++++++++------------- layers/composables/useApiData.ts | 22 +++++++ 3 files changed, 73 insertions(+), 46 deletions(-) create mode 100644 layers/composables/useApiData.ts diff --git a/i18n/locales/ko.ts b/i18n/locales/ko.ts index e7f3c28..7074d95 100644 --- a/i18n/locales/ko.ts +++ b/i18n/locales/ko.ts @@ -1,10 +1,11 @@ export default defineI18nLocale(async (locale: string) => { //https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 - const config = useRuntimeConfig() - const rootPath = config.public.staticUrl - const runType = config.public.runType + // const config = useRuntimeConfig() + // const rootPath = config.public.staticUrl + // const runType = config.public.runType - const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json` + // const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json` + const translationApi = `https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json` try { const { data } = await useFetch(translationApi, { @@ -15,7 +16,7 @@ export default defineI18nLocale(async (locale: string) => { }) // API 데이터에서 locale에 맞는 데이터를 추출 - const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 + const apiData = data.value?.['ko'] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 // API 데이터와 common.json 데이터를 병합 (common.json이 우선순위) const finalResult = { ...apiData } diff --git a/layers/components/layouts/Footer.vue b/layers/components/layouts/Footer.vue index 51c4c30..97cf995 100644 --- a/layers/components/layouts/Footer.vue +++ b/layers/components/layouts/Footer.vue @@ -144,20 +144,31 @@ diff --git a/layers/components/layouts/Footer.vue b/layers/components/layouts/Footer.vue index 97cf995..259cb8a 100644 --- a/layers/components/layouts/Footer.vue +++ b/layers/components/layouts/Footer.vue @@ -97,7 +97,7 @@
It looks like something broke.
+Sorry about that.
+$f*>7OTbHnE7smpPz2Rnk;9$HTz_~R3-gAzoN6{ z&j_kvx$P7|?O8F~DO>q`sq`G>0}olP4tGKmJC^xp2+LvdFfNJyG|6}rQ4$q?LV>8l z_D{~#yVbbsZb7!q9l(u+WSMWm#UrK)XYx>PX RqSwFR Y8Tg+TY5Rk^y3)maWPz@>Nk^y zVC})N+3voB$$QzFhAojKW{Aty=kd6Tq#2hKHvy)6m$FGdO9qg`Kif6^>OKCv){Og2 zKkAq`CQGv0%%rm0<9Flmpr;|*(X|M)_SVw5$|+jMta)XyZ1vVyWU7_vJgtsROdp zWj8RvtIc0dU7CD1$B>FJ(ri{ N&6$W%b E6XVhA|3rP(KU %mfmu5C2FZ?n{T+Twq(^#0 h6gVP+41Pq(BQN`N z^|RVITx|zk3A8LSg-$N8ySPrC<5OnlYrZ~}$XYLL2~>X^jlRNx>q01<8-(xAuB$Zg ziC#PDGSg;e22qnUF8s(4)%7{#bP4fYj0SUtpTew7{a>a~kI73%IF46npTTdynwH10 zrd0E0W;EsVQmNAIDwk}yf?9hZ$E{{^#x*g?7yF@dxg$XV_~Nx^18Zwe9C !Fde3`XYfROwL94ko9) z2#y=YDLW2y4jZy5`ifpYQ8T;qfre3nv71ayO~o^2UYEh~JuD{K0xyiHJ1-!eua*{^ zh6!GU>qN1EQ muGyF&ft)GG9NvK(R2xazp-p0w0*?JAtG(2YDj7491{C6H9=@v*{gQ`Pp5 z38ijBwuGZxQB5uivNhS)p6O{T%l8;~bk&aA8gg+p13Ip%N4rMFp(9#s@&fOP?^)(~ zm u2BTXn3+tKKIz~g)d@M&E1NF z7EgCwE7Z24ZLK }E&@qETFVEx+;GQLLZ2!75}h-1 D8;-6HMsh|P2}6eUXN2HVYNOR=C`O;6>soq& zKb#dkX78(sSga}GK|bIw=|WMmn1;JQ=3+LxWFv2~Iiy(ncFI9iALFnSz8M}o)7)iN zj$3e7*_f(zT+D}^*LCG$E!|GtHHSD7L0u2&fbU_t7Z76#&uD3?0|LUnqlUZb!DnSV z0Vilb1Nk2wg(qICyHDchT2ABToIlb?HWr%S`7X%&a)gD8nN#0-uGQm*xHVKvAG7vf zJkvq9;e_AqT@)Cm!W2VL6{P~9zqf#JHy6ktBV1GTE+ICqBsjz42bHJp1BdAQI&*bD zsv}mtnSm%FMr>}~* bXU&nflieh9`P>z5_IV8-hv@-TH?1-D{9yz$vXblPogBYNa^+(201Q896W z$s?evQE2jq*$d&)#(s?nmac3{vY7kPH73o0K3y?P5-&16ft)innYi@R@vesCM5S|? zp~Xq+ujt@yTAI^0-rit>lQ##wrinZNg>h *OL~~-s{ioBm?`6GYY^#o=b1F z&weM*o+KMzj3Rk-nlBCp%#ja ayu;Y~H&)8&~@h{}iG6y?g{@{sm z1aV4EQlx*LxiQO@Tm2EXnT=($b(e?1-@ CC8s7RjUqXh6C0*)eucrr9N`-a^0{$+YL8uCI}aYZ=r=HEt_lYQ!C$lem!Se z1HJC-dw3yi1qQ?3AsqYWx3*|>K4=$zqVMx-;p;Re3Pq{nVfg_^`JnyA`xn1P&d>v0 zzZk;uTLyI2+Pj%`-YJ&kso%oC3Q6Um;Jvg0zry;&nDejs(q1_k#MS6m%e$u$pMv)V zVD#mxgkdt9`i+YrfoRVmv965`r+&L~qtYzWrA5=_q|+I$@DfkNjrQ6Phec$IZp>mJ z!Pa_o)A5?rL6I?K#8eSsQ8H?2rOx*C>zi(NbaMTIE*e*Cp8tXxT%vk-+aa;OMxniI zQNmiXbG0o?4U U4_^5zr4Zr#W;)>8@=`}5OUMVZNqLj3@Q*Q1u08H>~*i750JS$jDY;;#@!`L zJ1kqm%$AA0qwm0Cu~NhxOxG`Cl0V4lBU06xt$&Yj_*hMKgGN*hq5I|Mhc_U{m+tGs zl<9`v*vT;zA?aeZkNknEu9ZRVC)zAQ#>B&7!zp@Z%D s^lam %`g9tU$xh2>J`CEhMfMTh1 zL@mTtH(~Cgd{+V59%|3WR4w!%LO1{tp^l}>hLSytMz{G>MLZmN&*|xZI?g@% 9z-1-N?BA- zyV4VO=-PbqT01$`jn;Y(^7nIOV7N_0*r)Z5W~FCD*U$Nq!Iy2$3HSQLER$wsJR2`; zMovfnZbwR6f7IZygK~MRQ Q@$8pN-pj$ z7Odk8)OMj=#Ol1{G#Dz9d+g(E{)yqUH8Jb7MYR1-6-y@-1Lt?QgX>2;|L(_~$zJ?W zb2)uPtt1u4Fvo2M3m#Yr)q8i0DGM3v{+uLbcaSe~*>*#Cf=unk#AM!Tg))5$g<7h| zZFsH`yJJn Z zqK*wB!DR~>21?MuvZyz7TPmys>4>6ydgk*OI+zF3i-&S*MirT+jW;-H((l7dx^a3i z1`4Se8!|P!Bhf}7mVgLQ*Ft58h(`kM*I$Jth8J%^YV%T 0=ti%UNo(N@gl>eAR!SymHjtlfDMa-F;wMRDmL&gxO_%)9jniK7HhO7# zIWC!|{WSuzDkA^i^HEl*Jw(goOSW9Rso+E6#(1a_`5$V4NF0aj-1U=V$diYhQ5`AC zhPiU=-Q+Ps`A3oJKYVtkL`=@I+d+|?2R1O4#|OePKnzQ?i8fOalJP0)w7B5`doULj z_6DS8rzOj=2!E8^$xIzPNa4D|1oM2T_DPCe@F;q=`mf{VLnhkD4i)lpMGM%j_d6*W zszL#oqy|y%QZ9c1#< wCyh^EIm3m +M#Ot w&JHG~f4a3h%8clUO7CQ%PJEI49*7VC z`@O#y?#?&%tWi>Pu?OaOpuH%sWC8}JzG)aS%11~2LPa|;heL}PCG_wgV?_c-MpETk zwj(QD1tnfKXsHs=iwKZf!G!506S$P|uhK^cx9Be0I -b z3752{`A-yac~EuT_KvH+@dfMU-_9QV&pOIH53gg;qrMsnpn7-a#gy107XQ69(A^sz zA(LW^4-5dga-B`MtE0ctNGY!(=9EIy_eeew8PcP)mE&&g8(4Rzh$2AA(p&1y{WQuy z#4f((4&moj>Nt4I=ZDr+*liq4<__&1-4&2J`p0*HafgHEIY{8@C=)tx22t*Rw=Z-@ zlh_qi%I%;vr=Yrbhi2p^;}gon6!S}g`S-jS8I~V>;Ati_gJYHoVaz=Sc~(0uf}c_0 z_oFl%bu}12RX()607=i0t4ICkUkR;56m?s%z&=~?#erc+v(m$| PGQGrCif{ZM8nkzO{lRlMWgI`EFOW$M7{HHGoZ*`_$Lw30+_oJyQfW1T4&E$@ zHd4(0X|ceVZ-1pJqUyGdRKOy~S_)Pm(WHIkCg8i;IaDbuWVBKuCmvk#XIz|sTLG#~ zX~{bbJtB*FSP j{WWw7CYrV#f*x%AEv-omSDE$qA8_vJzELQvdnlU+k z_Iyy-`9V}Z-9LzGhPS_c!_3T>7&h6wfxgBI4>%weSI}J}#;qe{62X>X$#hp7;`UgD z4=$bp=G;NWl(>X>6D<%D*|F=W?X`A4|MACk+4ej?)YCQ8nw*>-5X;(iEs>e_t%M z6Fs0s!97jqyH}_FwwQ3;Er5;&J8()h|1eJ@nnq-#Bu7mSa4`J`jL%iNw89cnDvOE( zHcLN3742UAT{2Y#+XP+QWUS9&kTm)qLFuXD_0nrC* Y{ha*esD!%Kdpeel6>b z!|x mO|9J0E=6>iK+?zUS33IvF1BSdzul)m`@XVn%KYy}W zNHzm3G6CuyfxN!<9#+Q8W^49vIAN$yl3_KixFJdqo+GBpZW9zVXvj1oDMI)vy~SO> zLU{YrBkMl5gFZr6Ztm~~%L`tO5CL$1>>S^qh#OF4FmQ9{8 0K7wY*oZbW}&YvAvGzdpozDLFUB& z&rgS-j^wVYTW@T2CuD0Mh2-Y}NFW!YYLk`v;U6Jv{6&q#c#B z3^JQbN+;ioqkyzAmk^3k;r}0;>uWg(=}Mb8moaO%8!)Dh2&=~Z7A_7 #u05~4O$p5quS_PF96u5WK{5_cL29oGN zFTS?cDF>N}BP2{JZ4k~hhi~LBSZ(S7X JvIh(n-^?&NfcekOK=hCFg+o@5I7rP`tY)Xq!4Bx@2;YYeC+SwikJ{9m~A @0 g$di0KDNT8F16)UIu=%9r4Ve4sFySk;Ccm$AW z=j&~4O_Y%(&{e`>$A`^|&2D7it;kFNig79pOZOn zTcoENwb^R2S?jI)Rq)g?{U>kfgGIttQU-0-r7WK5jiglQc8MqI48g~L-`P!mx0I)| zlct^uWH 9NO M_3@$X6-3 zq7@P?ZVmalwS1W+7rV6IzUe63aD=sP6K(gr%a0zM`PO2JIN0y8grLOH%|Srj28Fi0 zlRRPo@)>m%jFt*hw#VfiynUwPH2q*zRaND2HY$p*IEOE(#X-%4xRXd0lT4Evrrx-o zHus^-dNn@!OOIH`AbK!N*<)w>knex;<~J$k++hI^IMM-}HP?Oa8%j~`8?yT8qj}JJ z=6Kg9;%-f2VJu~x`y4xZFj?ie*g(a^M0T!*%<|99_7nL%;##!HY4Rhp2WtOPJTGsY zPm;%Hd)u4vP83sd^8yxIYu#7w8M>0;7?Xum66PfwlX#5g_HndWr6;><<-_=%6@spn z>Bd$7!RGdw`%tX%@yuDD_3d6F*T1Ag>e4~ykX&!I8{2CL(~c$5d7|uj!eDjaLYZ+? zEJ`H}Id=E~AHMd)c3>b}xfaaF&)KM8CE04_8@eo}I`RF7&%+z_QnZ(BHpJSt-ndz- zTz%X0t bp9AL+j0l;rxh1^dPCBy3pOVk9qAta*JM6>BN1dgJ{@ z{+DP-g@ZK$@(6>%2$41f8{bL&D{ju;?yYC-fdj(_W* {z0 z{9Y_-2p1P8AnO)SA+Sg9SnkNA_ojjo3ZYL6$;|F_$QswU>8!gC^)yrz-VF`QpZ^D| zTjFyTzK1FuoZ3%41#4L?_!`vB){^Q&g*vB=8VV=;PcxYs3t3p}a|7L3=a=3UjENXo zy$(&gjwxA7HlO=#K8hkRJ_b_*E);5CLTra}zLOoPCZ9BWm~~vbjI
vw9@5{u)JHDfB4X2l(uWF=T#_W05B69hhY&CtHby-!I?e6u?2HfPtD>APv-L4m zJ(^9s*>Kd)17}ZaB5IOR$IcGoh!`!AaEZA&I#p D&U3(Su~jO# z!N*-T>li|a4V5RnR2A>|;K?gcZaX`coZP0FOxJ)oP>n$sEBU;t|K|FZl61)FfnK3I z3%cE|4$oZ&cLP~3u>AY2!Bh1;TJ&SB$F=@yGtGl?+jv%dr!_Joraok&Y5gXuc8$MR zLfJ>gxii!;H*0pVCq*M#4^@m(7eCxxvxo?8%JI>Z16v2LTLn}F*xrr=b!C6o_HB!X z*lN8NEIJc0s~@KKlH8c9b55))tE+7)F6S91AWru>lg3w4;iBTC6DV0wL5Iw4z$#DI z>A@E^wWrPMMrYZrMtmQ1vKM&tkG=1s%JG7CxjQ;Q !@&-{}~DX0}jm8@Jh6 zvsLiPb+Fa^vDU >5)! FKQo#U 9iss)ieotmD%!@ z09SxC?pjjtx?XMf9Jy;~Yk~6kYpw?nUTiy^9v(V=^$OZsg1cCpQQ@{6v!)kY@_^G< z%?@UkZxF%6tRqHFqtsn;;dbKL>G|C3 y4`X^=39q>Bsr>AV4G8a-HPo zy{fWfu$NPVtehQWwQ9<8V=sM801~Ste2k$`H*$K4!8s?Z$zA3Sg55y2nLIuDAFIDH zk#5#GTmcw47RU79=`zbM+^rL)`iTc(O=Wh3{TCi26BE&fBV$ m-yug z+n=qn3Q#TChYD5*c0a9^N87(~>vM5GEk;0xOq5(PF%(rXhbc7|T;rYG&ZbIp_HLT0 zlld{VGJ0Vm&4vC?#C* hHN#o6yppf0kG?As<84GmF5hT zlZvnfdPDi&hPdkqO;kpX1p0aX=_pjxujE^96;7(Pg(rnog~=+nXAd#d@!5~nKxWktx!O>NPeOd`<-to;R{w2=j1eFv9Pue!~J2;oAgpE$Ax1cWFk8=H6 z_JNZxJktE#?&wuNRCFy-V_284_qd{&Ap5(k2!*(@eX%CH*r?K2DC^w07Jdj>%&!}M z=6wIVi;du7or3lJFHx}Vp5F t3h^#~Vb<|UzIC5V>x*TJsUF|$mmr*26Uehn!>4wd0>Q`IQ1h|$ z!T*ARpKmb!+)~x$i-+r~M)kpM$Clz*bhd}Ne1ZE%M$m%@vgUxQW4ov8#~o*lWAMF7 zRrl-eY)Yq6@ZG;G2PskV^o2r!0W05Sb$&^>6`1U(tY)y4XQHF%+RWr}nNrspf|$bp z?27sC;q5?`ER 11=A13iTmWX6`tURM4toSrJeRo!E~U+$&VOAN-HAbsWp z8qx|en4r)B2USUr9NKl9MT2BGKS)X$;DIrtpPO?IJzF~LnIr4kSJsi*jVd$ZX;n)H z;d{+Fi#_UGcV8opFmU^t_qB9^l8ZZnz0hGAkOL4dJR>IPurI%@@OabJm<(2P2zA3J zYjZr1)zLKsc$M+4=n&QYz2TXGwrqx-m+otuyL|s~9FVOg+y@8;3OeYV0JrsXX8 zZ4T|aej1}o VUT^74`_&(D8Q9M5@DJ(6#y? z>*y+VW4NsMob}WRs?O#F-v+`TAx#lb95twhf}Bx`Vdc@PM0cxztEJs-)sLzBk1%O| z6?8TJ*HBD!n|*Heo=tzXp>0-p(8Bvj5XB!>gGmY;cfxglgW%E$ARDX{Y85K;Q);jX zjjXh~VLbWX6RknXDfQqkFtow6V6Mg>=`dH96<;i{TU}9=x`UC0S6iQOE8Un++Z$f) zIiPZ)rsVoc=HSAFcbk_VsE>Z{;r@PYck7}V3T=hXOdOIk{=4C!x177Bn6vrXZ5v3` zm)M%qvbn~J4r5sF%Qo?81qc@?lZuRbgTlbHdv3biM{WH&X1J8+Q;-6r!8r4hRRr-Z zh~g+?b~Q`K-bA?G@6RU|j|}TBNw-i1z&_4t04ZGEijr-3m#6@S!gKmE%zp 5(t_sVRPJaX*T^Wi$0r@1` zB`e=6vuwG?>-X1|%!aF-JSKlJhR6Lu$hs76F4*G~*#PGN#Q>E^3^^T4+$Mcyd$#E= z0Te?9TzNaH=VH#x+1uMo-Yx~~2I+cE=xIdv#Di(lXT;5FMR-GxMgf|@0Em 8AG zHG%Xjacj6zcHkP&yE18)q*04BLp_fIW)8_s{y(nm|0s@j-zX4MB`^fHkKPNDbDf7P z^Zs!nuGB$LML&7TZ2W*(EvE<`_r}GqU~8|vHe8HHD8`%& 6#t=>Twf_>8 ztOPXp{n4_CN*3rUtHrIM-(4rq!{Ir_p(T(Bv^h~J 8pgn(~!EC zLbM>zi1EvlIbgC9eISQ>%1m*?wo&Bh L3I^tmH+Ytpm#ulO zT$yV0+nGlGn^!`Xn}ct+9gXF)F6LK94D^%Add`nAUB@Wv ^t*uY1PR_9422a#FtD<(4Mjeu&!MWVo&J!LJ*%k^tV^-)&reUKa)5EXsO) z>5Ji&7ys_yz?WE?Ai_e=JDXQi&FRe^0PCbSMc`(AbVMUp-x^Jo-BDn?+_mzy9-B7+ z#UKk|75{*26>09lXQt-#V=ICn<%lgT?m+yc0Ng^=B|;E N9?`vUQU#A z(R(@GJ4OppDL9WY(7sRg&*&mPP_^20yd5erEajqu&_1;`G#b9Tiy(!mcanp+E@ggF zhENRxL_a@L(<`ZIFng$ax)52;A!Zb{5mzKA1A35$z3l{{Qg&thLRFS=?+-Z~Soat! z0~Zji5)u-8pO~mo#a_!~FkyTlwxQc*0&(^~_$>^H=_ChY5f|?kO<7^ su81r4vWNBdj3JCNSwvBGVT^povB}B-oM>1XEjfu+7Oz1F@y`mPf`THE* z7L3aQ;@ElrP>$l>n0{iV%oQ~*pr+zdU@$pmbcrhb%{4*05Ss#6P3UUyJbsQP`Tkn^ zN}@&8t0x-vhm}lE?rZsH?LrCX$K@MaB16i!*_4(&c(89@5+6eWSGZhrK^`x_hZhj4 z+|A%2XQrcE)1kkv#riG<;d^;JTx02QcW-Y5KK*$XVL1F%!KMnZ(o5*kRSFiRebhhB z9?66em4dDsfJdA_(3LL}=aqvrN?K}qoLe!k%fIdcUp6$JM?9*((?O%M|3)?bxPt}2 z8K=*FEukm?Ui{nWJRh^I++6LUQcnKad&TVuLt|l#3APk+6MjnN)g*;fYrTuqxF`lz znD{RWh3!um=n%TtwB;A8auogRjVmnfhmAcGhOXo=s;#}{zel#46jKM-&-G>%b^pCN zW!7+pzP>)Ny7AFumCCCZ05+IvAorGf;`3U7>EpH( RU?9S)5pf2?B_nCk4qW(^ie1w&wPg z4v)+LU#Bm4BOmjdN% (nzog`=VcM#IfN$Vy_$=8!T|z zPBS!)G6Pm39MwN20*5s%yk!WheEs{Qv?)T=N6uqz_PzRtJjP<)`dhDJy+i<5x+m7? zvymU0TKyI*ZZcxk7Ib|2FTe4__JHTHqsd`Mb&7u5r&)lIljAqsiR-FopHu^PV8ksL z@hXxag#B_X1}iXaGb@RG{`8){^vl$JDecF)FU#}YHtrJi2R2S+?{OoL>aWn<*I1N~ zM&70esxQM2b(D`M8oIS=M1h2?_SaS-v?VPEI08WW8e;@N?G#U20 RJl1lio z9dExZG1K5dvSvl2+IFGCN%kv{f_ tyMa{`Q82^F^W#EzLP^UReV&7 z|NO% *W(j&`$;e~YhLg5dEQbKKdjJ=S#duXPZ+z@Afdc@ cc*Ff@x#d;DdgyfCW!9aW?4q2#3?esSWoC7=&zgZ(XtQ8I<)vY4;7lt!jxReEwle vAe z^?vwlYP6#2PDAS#5~-1ts9UI9D95-*3cq7f;+Sc6udnq^#nHbk+FQ;)NpV;H$~``& z=4;r*$xv?QMuSN_bI#1v-6L|>8E)a}$MQ(aTBCBaNwa!aQ_Vxe601hEd-6;2*`Spa z4YOE2KNHC4w pa|8r@BlTpQr!ML!Tr=jrfu7_ct{$J5hVy24KOq$9gi|h_G z{<}{d?77B1&TNNeH=8iFJyt^C%S&~!_T1c2!JoED%&{X?Yhd9745G$+wSWF6o0+5u z^wCoCluCFmSSVg@-dZ^B?)^DUP%z!A+ZTb&p{eLae!P2^?ta^UaV7yn{Om=aQ&3N|k&xJO7@Pcd`haBtTVY zj1cyAgvU~KB!)h;WlAZLNX{+ijg!G1l2)yaYy%qaJ%T%cV?HzjA-YT!iZ0lYY3JfX z>AX@~;aKF(_7x_z=Z5e8QjR=}HxwufqJuJ`(%deB4A7pJanW{3_lpXSZzRcI4|=Ko z@ Ui$sgy6zj;x2i=#gw_8XEUYC?1iq#-aVKe?p?S1ho6?__T0WM3~XVoikqf zz8mnPaY0`X1=w>tTiYesBQkxXnI}-|YjXzE0(m+ku;-;LX}J$C{yINdtXVhg#%p0F zq2p~XU)j+H2{i@#*v96h!lR8wOY0eW$nXe!$<1<4ZFp) &v)lE z{3pL4>tT|R`G+1zr+H!UMiGCRofFkx&x-0h&EGx=mL)B+nolz1g&h1YcOnz(0<+F4 z?zI|7;2tT>#c4JL#q~#XekLo0Exo+%tjMW!9{co4oA _b#i^)PEJs>f88entx{#u~HNAzrDi@GYY&ZNnH$Ex8ADt#1 z7vNqTr-N7cf~i72uMJtcRs6Hz;FX})@RQ*_Ot~BWbpN0KTn$b+?!$=B|J)Tz9!|Q& z*EDH% &Nv9Bsfw*xR6(IY_F=FJWALLJ*iHJy hFKf^6+;ep*w`?(b#rrDeqoM-gD4C-?>d}?k4sav^H*am!Y0 8SRZ?Fd7u4d1Y_i#nv<60^Xgg28!qALryp5VR1lv}ff)6~I{Wu? zl#1f6o;< 5gSGSv 7Mbk0@)zat;?_Kp=2Dj9ccE7Y-HOOoWIQgyH3d3BhbLY&iLk=HSy zP)A|&yE|W1|JZOi@lXg(1{}CjoJ}*$lxy_X1nWgeJoPr7S{e0L4mQqc8ZR+Qxz+Xj zdvq$Y)8Ka2MdM H)ht37=qWBP%Vv~ zls#Hp+|$H|Dsc{}n26^X<{O=Q)|XkB;0w{6CT~(y6uF$`ssX+IK(Q=9mbc1DI^ag?Z{wTIZ17>jgQ)U zmR3S;7(ShpWCQseNMJ{UQpLbPD(RpVvKXz}cc7J2H<7A)4w*hvX*=8}sVdiq@;S3V zymGVCyaj?R+bpVKovZE#2ijHV#}23PzjV)h7G2P7GT@>vXxitk7Ymd+KdGSfY^ca` zv4qT7V~NtpJEit7! D*nKx2jfPSoHX@P(}cvz0z@YNQWE}s}d*|0CNXL+x0smkZ#@K@doG+_-JvdjaF zUa=?OqPXMuk2}UqgOmG8LazT=J9gMo3BUdrv)$&H8@PsdomppUGaj%z8QKn|BO#WO z@u6X+IjBJ~75AAFj2}S>aqbh9JUnJZkn|Nmg jx5|{)dmR#ciCt z7NJ>6r~4x7I=B+L{6^Qb4^8)dvQu(bQ`P>t#Ic|V4mw$4HuLi^&`{%3n6!mrOrEcn z?eTa$f!)ZK-3T>SUs2!|T~53_Cd&a=Naa>BX_X8=8<1UhFrVQ>MC&QkP5^gQPRh2d zV~X9-Sv`7**O5Pypbb0l(%K@z3FG#(*|g8tajiS53SS?vU5uX$D{L^Eu4} L8nrUVnX<*(&b5=*_3T*AeExgg^e~QaFnV;79J|ZOM&$65<@28@pXCHHPO73JEMG z@clilvtM&9(KR^CVM6rb+1wH{+YVXtSwq~7orY6!1SC)A)9Q`*aUx5ZA#8PWixfCV zB$;0^u5;RbnOGu}=umjExD})ZY^L5zv3EsO4oB*CGyW~9k5@|I8`no{U%01- @r5M zAGZH? Pp%X10(UL)gMP(A&mP%(-*^1~@8A+sl%KXblKA)A5Esa{1+8h-q zuX!hNJk71*cLgnTnCy`77X{fFMg9u>!>ZWt5O9U6D@;lj 5oW!DYY3u){L;rv5V*j6;`v319NuZ7((@(Wul#72_r@Hgwo|}1iNC2NC zN36Nu@F3RqLRKM1+o{gXSy9yiJ1HTNU9RJm{Hb#Vml}&j{q9=DHV0?vnK9oC6S!v$ zQ@m$s2FcAa+Rm9CWG99l9J{pqIT-v-2+XbeV~fi8eV2TL2H5qCY9^P_H+dMmS(ui2 zDQz-H^nn|~3;5Ll_Vv7Q_z-5pWER ZnaGZ z*D77=N}W{H?sHYtiBI}>&VCH7J?m-mpYuM `{4 zNESCt7gOo+LYg{8V=7v-z5KOR2}hTs@z~^Wi`<~CpFKeLXym dO{8 z_PyU@3MuV>SnoonIw$Bna-y9veibaG^S^#Qm2;OMG&OzMTyEIsmlSQ9{Au5U&h>@! z4KGWJC+I|;nDWj=+SrCPZ;5X-66bV6w+!}LfIi*xHnFw_pqE#>6zn_?{1m{g4|8>? z_2o*~{Vo=8KaCk;<6C@xa`?q^w8XKyM=Q41rcXlhS-^j%R5h&*J|-pWVEc}J6Gd6f zf-b+>V4#&cVyX!mba_u@gDo*`+fuHo4ATK^+{IE{<6cZ_GcI*tGX6LJ3mlR`JVnB0 z!>eD~Pxi+FRjP)Zz`tFjuk+i}br>4+de>Xuz>nSEYa70C&E s*X3Q9C zzG7^ZvBk_yA!MX%sb(yVWj0x6WF{01$<4m6A=?b230cPSJN+5I^9P*Ad3?_Kd>-%D z@;od1tcVIKDtjMOJ-4UWIXZ4qfCQEW-1Y2XnNmGZKd~N!qT-*TiNy;M+RkIkfLYvU zRUEZ3ne XWTn+v~=J1w#*I>S)6+-$`GH;4t6 z1gYofU_w8_c@a+eBdQR9r#x>K6vsP^C0Ezon`{3am@%@?d3sm?2Kyvn`PFSOM?#Qy z0m)83A|fgH_V9>@EX6|C9Iz#NepULl^)6|e?QM1(flf?@H24CobbWxQP}uZ3Ua?!_ zf (HA^ePBgyEqlX8>lh*1tnp za`;#;4o>xKs5iZKRpfSVhDhg8#W-?lCkxv(Bk%DeUjES-JF=#5Mo&tj(Uppm`0adM z(>dC7)#7JHXmjJw)GqUPev^#}sZ;f;MONqj68ZBQbv(*A23D`ayT1p7>)W#v(LFE6 z!!L~*)dN0!1z^cBs{qgPC-8wTYwTS)rrT#>-2iMMdZ#1wET)Jo`k&keEdRQ=QQ`CC zx3;EsgdARZ19tUo)8XSvDk?b#(=YSZj~)|?vaYsA2#lnRkQMns96zA=ZLLVs2+^L1 zXgSg!Vdl|1O$+oac i~ufSc^q3l z1;XHQ8!RJ|o-SlKvsmkE0(;=5`G~VO+26n8?H~{`zA>JT)1ZgsQ6jyIu9EJP=`R{# zJnZ|q#&KAf }wV~R* z@>VPSuM5i4$J14!mvYxlELSdA78$QTX_~X#+jPJ<_MenMD=3OeDCC`SYv_8W5nO65 zSzj=@R}XxeWxFu3+dKdpzxg^0J@X~&y2h=H!Kkl_9YzQpt&I^y8z(jpZG724w|J@} zTBNF&moFzUsJCUS15vbi$Q;EBKWF#otkrAyJXw%41+_*sFX0p8pEPzlJy2>QZTPrL zxhXk{v6vXp62w@NQ5t6A2nzHaw&RqoF0fejbjoG0m8rB0G*8AKxzKyD57V7w{nztP z*TD(t-nSU7h1!%;+9S|h{YRwkvytRSy1FA-*GMbV(My3Ft$^}6$C~)3|6@rt*UhEL zwwR}-J62BcUt4^}D1B>Jg$It{a(&)bS5XFDTe(giw5IOS;Cso3JG3y1IoCCvL16Gb zCl?`rKFK%)oC!CDy?wxh+aA+aVomf=POw1+7l<*B)1F9+h2Fi};7V1CB>uKOz z%KPr |bl`E=;Oh!akFr1%t>h$~ z|Me}Go$T*Wfy8-IBm0@+KV6>~eTox@*;+rH$((|1Xt}#7<#?6iO*qHYsI=;a6jP40 zmV~1~i&pU==L6p5G;L*A@TxJv2(3?ot>13r5>k^};COf*gSI+c@l6NJDMPdUJ6q{} zmHtk^7 ULz)=GP$P>?LBQ$auY@;1|H6|yk#_L&v z8)u-ul9f~$Gcabnn750l^jV{?Elwp}DV@r}P{O3JhknAtuSOP%QPUr jI(u6^bwK$@H(2$az5((Ww6 zOAGrbJD#fE)cr*zHV6<8GckR>M75;P31 eqa TTV$uQBCLsVKlQX8>G#z7lz2zw@UR^ zz(X}N99m?Wl^ij2=v_pLV+)ANl)dUMrxxX1xNC^2F(I_4O|~hSM!;hk5beY`%&RdQ z_d66G)ZW()DU;*?9Al^kS+yg(Uczn`U)2Mqa*iy_Vl({~C{xRc<@^ppG6(6}@eiqP z>)K6J`P9OEt;NE1$8Lkg_l+oI2NhyYtcFZche*{S%cU_93An>C%_b${J*|Xb3QJwW zv)All;TMVP_P!=PJM(VMYpeA#kusy-u`uOQE@4eiUpL)$yY%@;wEMao#nh!{G+D)T zX~Su-!WJH3t>gK73e``9P7gB=y&dfw&So43Jv(#Y?iEV?1-VSsnz4wNqwh5Bb;2N~ z33gA|KKad7TDNY5^;umNqhqC1fujulmkh12v7h%gN@rKg0)06Xk0u`GDq2vxp$1Wv zP7SPwn )ekXh}dB z51ABW)-ockne0#C=Fa>NZ!?lz@`?L_VzP_3@Tvx(w}1%Kz7b_0qw_*lVe2;{>(jLL zLIPna8I@lVY2eRGkoPDZy8S)n2F-W*{fa}a*GyuztlB9DX`|8*$Xay@WG&^6xtrBp zsLK7tit9nI7_=4VN%ByL%Te%SpGm4s&EvIyH3P)u(_bhsm?2~2cfB!&QKEg!snV@< zxfT^%Y*__0>oBJz?CurqxLq?aIQM80ze2%N>f*UlR$@`ug7#R&^|kt0S7w-j06rS1 z?>cyA44guJTcP#~gmJV{K1dpB@n0{;aDZL3ehaz}QR5Hm+i^|zDQtJ`fG@YUl=$u{ zXBnn85xlcz3#A5E4`cqDB3C)nj=jP<#kS2qJ1nRp$#ukZC??SECY0O*9H28_O@kMf zup*rR*7fLtalR8(x9><$es(c}0MsfHxZ}Mdw$fEsVTA|-%G1!R+HhU~lIH2_J<^Zu zn=8AS*5B`8P@Hk_b7is^^;e{i_gFSkPyCJe8vcN=a~D33r;J~e`&Nbts(#<&jA Yu*kKsspHSHPMGurCe>BsUt&%Xn2K Cq8#< zXyoSUjmzlTzpLt5q449}mpv>~VQm|FZ#G jOEoZS|U}1Bn@H zys-{LPs|6a2Z^U1S{E4`1tQ*EavhNOnar`+4<(k76m0`EbBeQ_?2%-3Xp z= z2OIdWZ^rN)OtAAuN}QfOxhgA{f_D?E0(KOR;8>%hPHRI{`ihscxLm@s1kyX161R-u zerMMl`rn| ;Z zvd_2mTbi1-bbm8HY)9d~P1~SD2VSzYUjv#=>8nsk^*5&|0hM4;*PU{H=okyr>N~~7 zLaIgctpQY6#`0(q(XW{sKa=Q61XJYkjYWxW3o;l;WfagZ=oZ}mD5K@V@K~$6kq}ya zt e=eiv-5Zp2{%x_8Qs zKcm8CL?ue*@~i*ef_L3}yl|-Re&7VqiTOzNU|{LoSar$$QsgbqLt!69PIRizC+<&k zmi^y&rOsE5I^_~X`oI7e`tZ`_RAbCavJExl^c5W|NGj^<2zg@m>r3bd@RK%wi JY}DZ^W23<=L!N6HSv_Tg?f>~Cy`?9khj#jq?qRRaYU@J^W0 zw{E;HifgnTC({Sg%DLn>=(DNz0nZG|!-mgQ?Ch>L5T~Fqy 8iC8%%@coXT zavDl@>9u)%bztxe2|km-GH6|*rdtR#e(>kcpZc^l$lL#K;>SM{u{Q@l9DqtdlEc7q zuec9=y>qEZe@XPX7cXgq@p$^&p-RJBeWgcAp7zO$y~*SRfvcjz?ZG3SG@g5H)VhTK zNFs-Z-o)VIkOSS*9ReY*JHQ11M|`twaguDhsekv{(r6$sX*kbzeD& 9+vK>r%piwN@Sd>|7uv^b!mi=#0mXG1InViOQfItjc(c??QXeTzTk*Z$xOp z0b(Lcf1PDX+${@Rc>1U6Qq`Oh^aOU#WuUbucwnV%`viJYCrD1$Xc!v#O;XxZXhn6f zjxD$Ypoz!*4E6m!9hjX@c`M$0TTES)Y18xR-Qil9vtL~2j|X~5(_VWOL7@9UGP4c_ z+tG&N&m~68OVoDx7TWXEEjC@+?)Cqb2eg^hdjx#F6EJZOOEfg)m*)_DJFe*>_e5RR zPyD>Ms5r}KV3gW=U^#N&&|UkVJfZ$ Ppl64T%aQ+~$CE|(# zx1~qRd0VNJGVRAYM`bg)Ib|f^CzyXQxQ5TL2$+0v6f13pvh`)PXaCU%cgtW{8(w^g z+=u4=8R#v81)QZaO6pW+cYDP w%QUTHv7O%IEdJ6ru*7|%v%Q(LCm(@*^U-3 z$FQ?s`wGy+ba2N&d-@-}O#PACzKbMIdGHST;$3!!I?f~eK ~e05)M#cB*?));lC?~22t8ZhdNyst#u7lL%X?+R z>vwPPW)Ea)pizxFZE&%G85?^sN`EkNaz{@(bg7E8V?QeEz>5gJ=jbZXR8qnb5?1aY zO@f}FIVCGv6m8COlUvMoG*`SRj+@Ty2@I%s#{eYD`>~|-fvNtBaycZ;^b3xV(SbNM z{>722*Noopo$v1GD;d7W!>^-~m)=y*v~l;H3La^(!E`Lt&0EBt$Ud@dN}(Gq?1l%s zv7q}Xm0w`3H$s<=Rhk{@j104GI;lDO6!%;$aWHx~bYJoYZ!j7&Km$9XhSw_epc~tY+5A|x8h<#dnolfAZ{dd4 zv6;Hq1{CKd*LA0g27Wj}PaT<{{EXk<(~!Dt5&LS$bfw7-0ZfXy%wt|63J4dYplREY z&Xgf%$l wqd8S-rAC7DbIX+ezTqxjKiX0vmL+>M^xNn5Sw-11HN{w ztS=9&R3o?fL-s5OIr^{hxEL ly|GCFQe$M4IRGn34q0D`x zfx{Lg89QQ@YmW|$LB<3ocb2krYp6R1%Y+BR<`TAd|M+m?q5Yvh#iB*IEPWFHibbiZ zhrKl2aBpr7K3&23(eGK!$_BB8F`t434&3Y+S|!W39g6tAeoHX?l<1d+ZWx`IL99j@ zLcG1f>rdFKZnZ)Q{?LptZkTnakHl_S;W0XtEoMPL59|No_2J~^o@lI=gpsB3v9jOK OfBlNXWx9=5{Qm*pGv4d~ literal 0 HcmV?d00001 diff --git a/public/images/common/logo-stove.svg b/public/images/common/logo-stove.svg new file mode 100644 index 0000000..208d0a5 --- /dev/null +++ b/public/images/common/logo-stove.svg @@ -0,0 +1,3 @@ + + From 1003a01dee2953ddd0a6bd2ae730b2720239315e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chyeonggkim=E2=80=9D?= <“hyeonggkim@smilegate.com”> Date: Wed, 29 Oct 2025 20:56:58 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=A0=90=EA=B2=80=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .npmrc | 2 + .nvmrc | 1 + app/pages/error.vue | 9 - app/pages/inspection/index.vue | 325 ++++++++++++++++++ error.vue | 244 ++----------- layers/composables/useGetGameMaintenance.ts | 97 ++++++ .../useGetInspectionDataExternal.ts | 69 ++++ layers/layouts/inspection.vue | 6 + layers/middleware/inspection.ts | 42 +++ layers/middleware/pageData.global.ts | 14 +- layers/plugins/error-handler.ts | 12 + layers/server/api/clientIp.ts | 11 + layers/server/middleware/gameData.ts | 56 ++- layers/stores/inspectionStore.ts | 36 ++ layers/stores/useCallerInfoStore.ts | 13 + layers/stores/useCommonStore.ts | 115 +++++++ layers/types/Common.ts | 22 +- layers/types/DataizationType.ts | 101 ++++++ layers/types/GameMaintenanceType.ts | 44 +++ layers/types/InspectionType.ts | 38 ++ layers/utils/commonUtil.ts | 168 +++++++++ layers/utils/dataUtil.ts | 47 +++ package.json | 2 + pnpm-lock.yaml | 38 +- temp/middleware.ts | 286 +++++++++++++++ tsconfig.json | 2 +- 26 files changed, 1553 insertions(+), 247 deletions(-) create mode 100644 .npmrc delete mode 100644 app/pages/error.vue create mode 100644 app/pages/inspection/index.vue create mode 100644 layers/composables/useGetGameMaintenance.ts create mode 100644 layers/composables/useGetInspectionDataExternal.ts create mode 100644 layers/layouts/inspection.vue create mode 100644 layers/middleware/inspection.ts create mode 100644 layers/plugins/error-handler.ts create mode 100644 layers/server/api/clientIp.ts create mode 100644 layers/stores/inspectionStore.ts create mode 100644 layers/stores/useCallerInfoStore.ts create mode 100644 layers/stores/useCommonStore.ts create mode 100644 layers/types/DataizationType.ts create mode 100644 layers/types/GameMaintenanceType.ts create mode 100644 layers/types/InspectionType.ts create mode 100644 layers/utils/commonUtil.ts create mode 100644 temp/middleware.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..67673ab --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@seed-next:registry=https://git.sginfra.net/api/v4/groups/4424/-/packages/npm/ +# @stove-ui:registry=https://git.sginfra.net/api/v4/projects/557/packages/npm/ diff --git a/.nvmrc b/.nvmrc index 7af24b7..3bee07e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1,2 @@ 22.11.0 + diff --git a/app/pages/error.vue b/app/pages/error.vue deleted file mode 100644 index 1068f43..0000000 --- a/app/pages/error.vue +++ /dev/null @@ -1,9 +0,0 @@ - -+ - - \ No newline at end of file diff --git a/app/pages/inspection/index.vue b/app/pages/inspection/index.vue new file mode 100644 index 0000000..afc0268 --- /dev/null +++ b/app/pages/inspection/index.vue @@ -0,0 +1,325 @@ + +--Dang
-It looks like something broke.
-Sorry about that.
-+ + + + + + + diff --git a/error.vue b/error.vue index dc92961..4055e3d 100644 --- a/error.vue +++ b/error.vue @@ -1,221 +1,25 @@ + + -++ ++
+ +++ + {{ webInspectionData.inspection_title1 }} + + + {{ tm('Inspection_Now_Maintenance') }} + +
+ ++ ++++ + +{{ tm('Inspection_Maintenance_Time') }}
++++ {{ getLocaleTimezone(locale) }} +++ {{ getLocaleTimezone('en', 'US') }} +++ {{ getLocaleTimezone('zh-tw', '') }} +++ {{ getLocaleTimezone('ja', '') }} +++ +++ ++ + ++ + {{ tm('Inspection_Community_Btn') || '공식 커뮤니티' }} +++ + + {{ tm('game_start_btn') || '게임 실행' }} + ++ +++ {{ tm('Inspection_Txt_Download') || '게임 다운로드' }} +
++ {{ webInspectionData.inspection_content }} ++-- - - - - - \ No newline at end of file +- - ------- - -
- - - {{ tm('Error_Official_Page') }} -++ + + diff --git a/layers/composables/useGetGameMaintenance.ts b/layers/composables/useGetGameMaintenance.ts new file mode 100644 index 0000000..a9835b3 --- /dev/null +++ b/layers/composables/useGetGameMaintenance.ts @@ -0,0 +1,97 @@ +import type { ReqGameMaintenance, ResGameMaintenance } from '#layers/types/GameMaintenanceType' + +/** + * 게임 점검 + */ +const useGetGameMaintenance = () => { + const inspectionStore = useInspectionStore() + const logPrefix = { + exception: '[Exception] /composables/useGetGameMaintenance', + failure: '[Failure] /composables/useGetGameMaintenance' + } + const isGameMaintenance = ref(false) // 게임 서버 점검 여부 + + // [Setter] 게임 서버 점검 여부 세팅 + const setIsGameMaintenance = (status: boolean) => { + isGameMaintenance.value = status + } + + // 게임 점검이 아닌 경우 일괄 세팅 + const setGameMaintenanceFalse = () => { + setIsGameMaintenance(false) + inspectionStore.setGameMaintenanceStatus(false) + inspectionStore.setGameMaintenanceData({ ts_start_date: 0, ts_end_date: 0, detail_link: '' }) + } + + /** + * 게임 서버 점검 여부 + * + * @param {ReqGameMaintenance} req + * @description https://wiki.smilegate.net/pages/viewpage.action?pageId=362619887 + */ + const checkGameMaintenance = async (req: ReqGameMaintenance) => { + let res: ResGameMaintenance = {} as ResGameMaintenance + try { + const baseApiUrl = req.baseApiUrl || '' + + // Path Variables + const category = req.category || 'GAME' + const serviceId1 = req.service_id1 || '' + const lang = req.lang || 'ko' + + const url = `${baseApiUrl}/v2.0/maintenances/${category}/${serviceId1}/${lang}` + + res = (await commonFetch('GET', url, {})) as ResGameMaintenance + + if (res != null && res.code === 0) { + // FIXME: 테스트용 데이터 --------------------------------------------------- + /* const config = useRuntimeConfig() + if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) { + res.value = { + total_count: 1, + list: [ + { + start_at: new Date().getTime(), + end_at: new Date().getTime(), + languages: [{ link: 'https://www.onstove.com', lang: 'ko', title: '', content: '' }], + maintenance_no: 0, + category: '', + service_id1: '', + service_id2: [], + type: '', + description: '' + } + ] + } + } */ + // ------------------------------------------------------------------------ + if (Number(res.value?.total_count) > 0 && res.value?.list != null && res.value?.list.length > 0) { + setIsGameMaintenance(true) // 서버 1개 이상 점검일 경우 점검 중으로 간주 + inspectionStore.setGameMaintenanceData({ + ts_start_date: res.value?.list[0].start_at || 0, + ts_end_date: res.value?.list[0].end_at || 0, + detail_link: res.value?.list[0].languages[0].link || '' + }) + inspectionStore.setGameMaintenanceStatus(true) + } else { + setGameMaintenanceFalse() + } + } else { + // [500] 내부 서버 에러 + // [70001] 부적절한 엑세스 토큰 + // [70051] 부적절한 파라미터 요청 - {param_key} + // [70052] 데이터를 찾을 수 않음 + setGameMaintenanceFalse() + } + } catch (e) { + console.error(`${logPrefix.exception}.checkGameMaintenance: `, e) + res = { code: -99999, message: `${e}` } + setGameMaintenanceFalse() + } + return res + } + + return { isGameMaintenance, checkGameMaintenance } +} + +export { useGetGameMaintenance } diff --git a/layers/composables/useGetInspectionDataExternal.ts b/layers/composables/useGetInspectionDataExternal.ts new file mode 100644 index 0000000..c3a2e46 --- /dev/null +++ b/layers/composables/useGetInspectionDataExternal.ts @@ -0,0 +1,69 @@ +import type { WebInspectionData, ReqGetInspectionData, ResGetInspectionData } from '#layers/types/InspectionType' + +/** + * 웹 점검 + */ +export const useGetInspectionDataExternal = () => { + const inspectionStore = useInspectionStore() + const logPrefix = { + exception: '[Exception] /composables/useGetInspectionDataExternal', + failure: '[Failure] /composables/useGetInspectionDataExternal' + } + const webInspectionData = ref{{ error?.statusCode }}
+{{ error?.statusMessage }}
++ Clear errors + +(null) + const isWebInspection = ref(false) // 웹 점검 여부 + + // [Setter] 웹 점검 여부 세팅 + const setIsWebInspection = (status: boolean) => { + isWebInspection.value = status + } + + /** + * 웹 점검 여부 + * + * @param {ReqGetInspectionData} req + * @description https://wiki.smilegate.net/pages/viewpage.action?pageId=563198067 + */ + const getInspectionDataExternal = async (req: ReqGetInspectionData) => { + // const config = useRuntimeConfig() + const apiUrl = `${req.baseApiUrl}/pub-comm/v3.0/inspection/${req.gameId}` + + try { + const response = (await commonFetch('GET', apiUrl)) as ResGetInspectionData + + // FIXME: 테스트용 데이터 --------------------------------------------------- + /* if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) { + response.value = { + inspection_status: 1, + inspection: { + inspection_status: 1, + start_date: '2025-09-19 10:00:00', + end_date: '2025-09-19 12:00:00', + ts_start_date: new Date().getTime(), + ts_end_date: new Date().getTime(), + back_ground_image_type: 'image', + back_ground_image_url: 'https://www.onstove.com', + inspection_title1: '', + inspection_title2: '' + } + } + } */ + // ------------------------------------------------------------------------ + + if (response?.value && response.value.inspection) { + webInspectionData.value = response.value.inspection + isWebInspection.value = response.value.inspection_status === 1 + + inspectionStore.setWebInspectionData(webInspectionData.value) + inspectionStore.setWebInspectionStatus(isWebInspection.value) + } + } catch (e) { + console.error(`${logPrefix.exception}.getInspectionDataExternal: `, e) + } + + if (webInspectionData.value !== null) { + setIsWebInspection(isWebInspection.value) + } + } + + return { webInspectionData, isWebInspection, getInspectionDataExternal } +} diff --git a/layers/layouts/inspection.vue b/layers/layouts/inspection.vue new file mode 100644 index 0000000..ddd7e8c --- /dev/null +++ b/layers/layouts/inspection.vue @@ -0,0 +1,6 @@ + + + + + + diff --git a/layers/middleware/inspection.ts b/layers/middleware/inspection.ts new file mode 100644 index 0000000..e1ecbd4 --- /dev/null +++ b/layers/middleware/inspection.ts @@ -0,0 +1,42 @@ +export default defineNuxtRouteMiddleware(async (to) => { + try { + if (import.meta.client) { + const config = useRuntimeConfig() + // const baseDomain = `${config.public.baseDomain}` + const stoveApiUrl = `${config.public.stoveApiUrl}` + console.log("🚀 ~ stoveApiUrl:", stoveApiUrl) + // const stoveGameId = `${config.public.stoveGameId}` + // const stoveMaintenanceApiUrl = `${config.public.stoveMaintenanceApiUrl}` + + /* const localeCookie = useCookie('LOCALE', { + domain: baseDomain + }) */ + + // const finalLocale = csrGetFinalLocale(to.path) + // localeCookie.value = finalLocale.toUpperCase() + + // 웹 점검 ----- + const { isWebInspection, getInspectionDataExternal } = useGetInspectionDataExternal() + await getInspectionDataExternal({ baseApiUrl: stoveApiUrl, gameId: 'STOVE_LORD' }) + + // 게임 점검 ----- + // const { checkGameMaintenance } = useGetGameMaintenance() + // await checkGameMaintenance({ + // baseApiUrl: stoveMaintenanceApiUrl, + // category: 'GAME', + // service_id1: stoveGameId, + // lang: `${finalLocale}`.toLowerCase() + // }) + + if (isWebInspection.value && !to.path.includes('inspection') && !to.path.includes('api')) { + // 점검 중인 경우 + // return navigateTo(`/${finalLocale}/inspection`, { external: true }) + } else if (!isWebInspection.value && to.path?.indexOf('inspection') !== -1) { + // 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트 + // return navigateTo(`/${finalLocale}`, { external: true }) + } + } + } catch (e) { + console.error('[Exception] /middleware/inspection: ', e) + } +}) diff --git a/layers/middleware/pageData.global.ts b/layers/middleware/pageData.global.ts index af563db..abb60d9 100644 --- a/layers/middleware/pageData.global.ts +++ b/layers/middleware/pageData.global.ts @@ -16,6 +16,10 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page` try { + if(to.matched) { + return + } + const pageUrl = getPathAfterLanguage(to.path) // pageUrl이 빈값이거나 null이면 /brand로 리다이렉트 @@ -36,9 +40,17 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { loading: true, })) as PageDataResponse | null + console.log("🚀 ~ response?.code:", response?.code) + if(response?.code === 91003) { + throw createError({ + statusCode: 404, + statusMessage: 'Page not found', + }) + } + if (response?.code === 0 && 'value' in response) { store.setPageData(response.value) - console.log('🚀 ~ pageData:', response.value) + // console.log('🚀 ~ pageData:', response.value) } else { store.clearPageData() } diff --git a/layers/plugins/error-handler.ts b/layers/plugins/error-handler.ts new file mode 100644 index 0000000..54a402a --- /dev/null +++ b/layers/plugins/error-handler.ts @@ -0,0 +1,12 @@ +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.config.errorHandler = (error, instance, info) => { + console.log("🚀 000000 ~ error:", error) + // handle error, e.g. report to a service + } + + // Also possible + nuxtApp.hook('vue:error', (error, instance, info) => { + console.log("🚀1111 ~ error:", error) + // handle error, e.g. report to a service + }) + }) \ No newline at end of file diff --git a/layers/server/api/clientIp.ts b/layers/server/api/clientIp.ts new file mode 100644 index 0000000..25a2958 --- /dev/null +++ b/layers/server/api/clientIp.ts @@ -0,0 +1,11 @@ +import { getTrueClientIp } from '#layers/utils/apiUtil' + +export default defineEventHandler((event) => { + let clientIP = '' + try { + clientIP = getTrueClientIp(event.node.req) + } catch (e) { + console.error('[Exception] /server/api/clientIp - Cannot Get Client IP: ', e) + } + return clientIP || '' +}) diff --git a/layers/server/middleware/gameData.ts b/layers/server/middleware/gameData.ts index 3857a86..480b2cc 100644 --- a/layers/server/middleware/gameData.ts +++ b/layers/server/middleware/gameData.ts @@ -6,8 +6,13 @@ import { } from 'h3' import { ssrGetFinalLocale } from '../../utils/localeUtil' import type { GameDataResponse } from '../../types/api/gameData' +import type { ResGetInspectionData } from '../../types/InspectionType' export default defineEventHandler(async event => { + + const config = useRuntimeConfig() + const iBaseApiUrl = `${config.public.stoveApiUrlServer}` + const url = getRequestURL(event) // 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리 @@ -36,24 +41,12 @@ export default defineEventHandler(async event => { const config = useRuntimeConfig() const stoveApiUrlServer = config.public.stoveApiUrlServer const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game` - + let inspectionData const langCode = ssrGetFinalLocale( event?.node.req.url, event.node.req.headers ) - // URL의 첫 번째 path를 lang_code로 사용 (파비콘, API 경로 제외) - // const pathSegments = url.pathname - // .split('/') - // .filter( - // segment => - // segment && - // !segment.includes('favicon') && - // !segment.includes('api') && - // !segment.startsWith('_') - // ) - // const langCode = pathSegments[0] || 'ko' - const queryParams: Record = { game_domain: event.context.gameDomain || '', lang_code: langCode, @@ -67,8 +60,43 @@ export default defineEventHandler(async event => { event.context.gameData = response.value event.context.googleAnalyticsId = response.value?.ga_code - console.log('🚀 ~ gameData:', response.value) + // console.log('🚀 ~ gameData:', response.value) + + // 점검 데이터 조회 + if (response.value.game_id) { + const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response.value.game_id}` + + // 직접 $fetch 사용 (composable 사용하지 않음) + const inspectionResponse = await $fetch (inspectionApiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + inspectionData = inspectionResponse?.value?.inspection + // console.log("🚀 ~ inspectionData:", inspectionData) + + if (inspectionData?.inspection_status === 0 ) { + /** + * 점검 중인 경우 + * - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ + * - 점검 URL 경로가 아닐 경우 no-cache 설정 + * - 화이트 리스트 체크 + */ + // 현재 경로가 점검 페이지가 아닐 경우 리다이렉트 + const inspectionPath = `/${langCode}/inspection` + + if (!url.pathname.includes('/inspection')) { + event.node.res.statusCode = 302 + event.node.res.setHeader('Location', inspectionPath) + event.node.res.end() + return + } + } + + } } + } catch (error) { console.error('gameData load error:', error) } diff --git a/layers/stores/inspectionStore.ts b/layers/stores/inspectionStore.ts new file mode 100644 index 0000000..12b3aa2 --- /dev/null +++ b/layers/stores/inspectionStore.ts @@ -0,0 +1,36 @@ +import type { WebInspectionData } from '#layers/types/InspectionType' +import type { GameMaintenanceData } from '#layers/types/GameMaintenanceType' + +export const useInspectionStore = defineStore('inspection', () => { + const webInspectionData = ref (null) // 웹 점검 정보 + const webInspectionStatus = ref (null) // 웹 점검 상태 + const gameMaintenanceData = ref (null) // 게임 점검 정보 + const gameMaintenanceStatus = ref (null) // 게임 점검 상태 + + const setWebInspectionData = (data: WebInspectionData) => { + webInspectionData.value = data + } + + const setWebInspectionStatus = (status: boolean) => { + webInspectionStatus.value = status + } + + const setGameMaintenanceData = (data: GameMaintenanceData) => { + gameMaintenanceData.value = data + } + + const setGameMaintenanceStatus = (status: boolean) => { + gameMaintenanceStatus.value = status + } + + return { + webInspectionData, + webInspectionStatus, + gameMaintenanceData, + gameMaintenanceStatus, + setWebInspectionData, + setWebInspectionStatus, + setGameMaintenanceData, + setGameMaintenanceStatus + } +}) diff --git a/layers/stores/useCallerInfoStore.ts b/layers/stores/useCallerInfoStore.ts new file mode 100644 index 0000000..368b3dd --- /dev/null +++ b/layers/stores/useCallerInfoStore.ts @@ -0,0 +1,13 @@ +export const useCallerInfoStore = defineStore('callerInfoStore', () => { + const callerId = ref ('') + const callerDetail = ref ('') + + const setCallerId = (paramCallerId: string | null) => { + callerId.value = paramCallerId + } + const setCallerDetail = (paramCalleDetail: string | null) => { + callerDetail.value = paramCalleDetail + } + + return { callerId, callerDetail, setCallerId, setCallerDetail } +}) diff --git a/layers/stores/useCommonStore.ts b/layers/stores/useCommonStore.ts new file mode 100644 index 0000000..d19df92 --- /dev/null +++ b/layers/stores/useCommonStore.ts @@ -0,0 +1,115 @@ +import { defineStore } from 'pinia' +import { useWindowSize, useWindowScroll } from '@vueuse/core' + +interface DeviceMode { + mode: 'desktop' | 'mobile' + browser: 'chrome' | 'crawler' | 'edge' | 'firefox' | 'safari' | null + isDesktop: boolean + isMobile: boolean + isTablet: boolean + isIos: boolean + isAndroid: boolean + isDeviceReady: boolean +} + +export const useCommonStore = defineStore('commonStore', () => { + const stoveGnbHeight = 48 + + const useDeviceData = useDevice() + const { width: windowWidth, height: windowHeight } = useWindowSize() + const { x: windowX, y: windowY } = useWindowScroll({ behavior: 'smooth' }) + + const device = ref ({ + mode: useDeviceData.isMobile || useDeviceData.isTablet ? 'mobile' : 'desktop', + browser: useDeviceData.isChrome + ? 'chrome' + : useDeviceData.isCrawler + ? 'crawler' + : useDeviceData.isEdge + ? 'edge' + : useDeviceData.isFirefox + ? 'firefox' + : useDeviceData.isSafari + ? 'safari' + : null, + isDesktop: useDeviceData.isDesktop, + isMobile: useDeviceData.isMobile, + isTablet: useDeviceData.isTablet, + isIos: useDeviceData.isIos, + isAndroid: useDeviceData.isAndroid, + isDeviceReady: false + }) + + const isPassedStoveGnb = ref(false) + const scrollFixedXValue = ref('0px') + const footerRef = ref (null) + const isLoading = ref (true) + const isScrollLock = ref (false) + + const updateDeviceMode = () => { + device.value.mode = useDeviceData.isMobile || useDeviceData.isTablet ? 'mobile' : 'desktop' + device.value.browser = useDeviceData.isChrome + ? 'chrome' + : useDeviceData.isCrawler + ? 'crawler' + : useDeviceData.isEdge + ? 'edge' + : useDeviceData.isFirefox + ? 'firefox' + : useDeviceData.isSafari + ? 'safari' + : null + device.value.isDesktop = useDeviceData.isDesktop + device.value.isMobile = useDeviceData.isMobile + device.value.isTablet = useDeviceData.isTablet + device.value.isIos = useDeviceData.isIos + device.value.isAndroid = useDeviceData.isAndroid + device.value.isDeviceReady = true + } + + const updateIsPassedStoveGnb = () => { + isPassedStoveGnb.value = windowY.value >= stoveGnbHeight + + if (isPassedStoveGnb.value) { + scrollFixedXValue.value = `-${windowX.value}px` + } else { + scrollFixedXValue.value = '0px' + } + } + + const isLoadingComplete = () => { + isLoading.value = false + } + + const scrollLock = () => { + isScrollLock.value = !isScrollLock.value + } + + const addScrollLock = () => { + isScrollLock.value = true + } + + const removeScrollLock = () => { + isScrollLock.value = false + } + + return { + device, + windowWidth, + windowHeight, + windowX, + windowY, + isPassedStoveGnb, + scrollFixedXValue, + footerRef, + isLoading, + isScrollLock, + + updateDeviceMode, + updateIsPassedStoveGnb, + isLoadingComplete, + scrollLock, + addScrollLock, + removeScrollLock + } +}) diff --git a/layers/types/Common.ts b/layers/types/Common.ts index 42d5031..4ef5537 100644 --- a/layers/types/Common.ts +++ b/layers/types/Common.ts @@ -1,5 +1,5 @@ import type { HTMLAttributes } from 'vue' -import type { StoveJsService } from '@/layers/types/Stove' +import type { StoveJsService } from '#layers/types/Stove' export type ClassType = HTMLAttributes['class'] @@ -8,3 +8,23 @@ declare global { stoveJsService?: StoveJsService } } +interface CommonRequestType { + baseApiUrl: string + gameId: string +} + +interface CommonResponseType { + code?: number + message?: string +} + +interface CommonPeriodType { + startDate?: string + endDate?: string +} +interface ParsedCustomLinkOptions { + tm: (key: string) => { txt: string } + query?: Record +} + +export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions } diff --git a/layers/types/DataizationType.ts b/layers/types/DataizationType.ts new file mode 100644 index 0000000..07faafd --- /dev/null +++ b/layers/types/DataizationType.ts @@ -0,0 +1,101 @@ +import type { PromotionPreregistType } from '@/types/promotion/PreregistType' +import type { CommonPeriodType } from '@/types/CommonType' + +// [S] Type in czn_homepage_brand_siteConfig.json ---------------------------------------- +interface GnbMenuType { + id: string + title: string + link: string + target: string + displayLocales?: Array +} + +interface GnbType extends GnbMenuType { + depth2List?: Array +} + +interface SnsType { + id: string + title: string + link: string + sub: string + key?: string + log?: object +} + +interface LoreType { + loreNo: number + chapter: number // 1 : 프롤로그, 2 ~ : N장 + title: string + description: string +} + +interface CharacterCardType { + id: string +} + +interface CharacterType { + id: string + cardList: Array +} + +interface FooterMenuType { + id: string + title: string + link: string + target: string + active: string +} + +interface MediaType { + id: string + title: string + logCode?: string +} + +interface MarketType { + id: string + code: string + link: string +} + +// [E] Type in czn_homepage_brand_siteConfig.json ---------------------------------------- +interface ReqGetDataization { + baseApiUrl: string + fileName?: string +} + +interface DataizationType { + gnbList?: Array + mainVideo: CommonPeriodType + promotionList?: Array + characterList?: Array + loreList?: Array + footerMenuList?: Array + mediaList?: Array + sectionList?: Array + marketList?: Array +} + +interface ResGetDataization { + code: number + message: string + value?: { + dataization?: DataizationType + } +} + +export type { + // [S] Type in czn_homepage_brand_siteConfig.json ---------------------------------------- + GnbType, + SnsType, + MediaType, + LoreType, + PromotionPreregistType, + FooterMenuType, + MarketType, + // [E] Type in czn_homepage_brand_siteConfig.json ---------------------------------------- + DataizationType, + ReqGetDataization, + ResGetDataization +} diff --git a/layers/types/GameMaintenanceType.ts b/layers/types/GameMaintenanceType.ts new file mode 100644 index 0000000..c7dae68 --- /dev/null +++ b/layers/types/GameMaintenanceType.ts @@ -0,0 +1,44 @@ +import type { CommonRequestType, CommonResponseType } from './Common' + +/************************************************************************* + * 게임 점검 + ************************************************************************/ +interface ReqGameMaintenance extends CommonRequestType { + // Path Variables + category: string + service_id1: string + lang: string +} +interface Language { + lang: string + title: string + content: string + link: string +} +interface GameMaintenance { + maintenance_no: number // 점검 번호 + category: string // 카테고리 + service_id1: string // 서비스 ID1 + service_id2: Array // 서비스 ID2(String Array), service_id1 전체를 설정할 경우 ["*"]로 등록해야 함. + type: string // 점검타입(REGULAR / TEMPORARY / URGENT) + languages: Array // 다국어 리스트 정보 + description: string // 설명 + start_at: number // UTC기준 점검 시작일(milli-timestamp(13digit)) + end_at: number // UTC기준 점검 종료일(milli-timestamp(13digit)) +} +interface DtoGameMaintenance { + total_count: number + list: Array +} +interface ResGameMaintenance extends CommonResponseType { + value?: DtoGameMaintenance + error?: string +} + +// 게임 점검 데이터 +interface GameMaintenanceData { + ts_start_date: number // 게임 점검 시작 타임스탬프 + ts_end_date: number // 게임 점검 종료 타임스탬프 + detail_link?: string // 게임 점검 공지 링크 +} +export type { ReqGameMaintenance, ResGameMaintenance, GameMaintenanceData } diff --git a/layers/types/InspectionType.ts b/layers/types/InspectionType.ts new file mode 100644 index 0000000..69ed9ae --- /dev/null +++ b/layers/types/InspectionType.ts @@ -0,0 +1,38 @@ +import type { CommonRequestType, CommonResponseType } from './Common' + +/************************************************************************* + * 웹 점검 + ************************************************************************/ +interface WebInspectionData { + inspection_status: number // 점검 상태 (0: 정상, 1: 점검 중) (단순 운영툴 설정 점검 값) + start_date: string // 점검 시작 날짜 (문자열 형식) + end_date: string // 점검 종료 날짜 (문자열 형식) + ts_start_date: number // 점검 시작 타임스탬프 + ts_end_date: number // 점검 종료 타임스탬프 + back_ground_image_type?: string // 배경 이미지 타입 (0: 없음, 기타 값: 특정 타입) + back_ground_image_url?: string // 배경 이미지 URL + movie_yn?: string // 동영상 사용 여부 ("Y" 또는 "N") + movie_url?: string // 동영상 URL + inspection_title_type?: string // 점검 제목 타입 + inspection_title1: string // 점검 제목 1 + inspection_title2: string // 점검 제목 2 + inspection_content?: string // 점검 내용 + + // Internal ----- + ip_filter_use_yn?: string // IP 필터 사용 여부 ("Y" 또는 "N") + ip_filter_list?: string[] // 허용된 IP 목록 +} + +interface ReqGetInspectionData extends CommonRequestType { + // do nothing +} + +interface DtoGetInspectionData { + inspection_status?: number // 점검 여부 + 점검 시간 + 화이트 리스트 고려하여 계산된 결과 + inspection?: WebInspectionData +} +interface ResGetInspectionData extends CommonResponseType { + value?: DtoGetInspectionData +} + +export type { WebInspectionData, ReqGetInspectionData, ResGetInspectionData } diff --git a/layers/utils/commonUtil.ts b/layers/utils/commonUtil.ts new file mode 100644 index 0000000..1d22531 --- /dev/null +++ b/layers/utils/commonUtil.ts @@ -0,0 +1,168 @@ +import type { ParsedCustomLinkOptions } from '@/types/CommonType' + +/** + * 페이지 - 유효성 체크 + * + * @param {number} page - 페이지 + * @param {number} totalPage - 총 페이지 수 + */ +const checkPageValidation = (page: number, totalPage: number) => { + // 최소, 최대 범위 체크 + if (page < 1) { + page = 1 + } else if (page > totalPage) { + page = totalPage + } + return page +} + +/** + * 파일 다운로드 함수 + * + * @param {string} fileUrl - 다운로드할 파일의 URL + * @param {string} fileName - 저장할 파일 이름 (옵션) + */ +const csrDownloadFile = (fileUrl: string, fileName?: string) => { + const link = document.createElement('a') + link.href = fileUrl + + // 파일 이름이 제공되면 다운로드 이름 설정 + if (fileName) { + link.download = fileName + } + + // 링크를 클릭하여 다운로드 트리거 + document.body.appendChild(link) + link.click() + + // DOM에서 링크 제거 + document.body.removeChild(link) +} + +/** + * 마케팅 코드 조회 + */ +const csrGetMarketingCode = () => { + const route = useRoute() + const mcode = Number(`${route.query.mcode != null && route.query.mcode !== '' ? route.query.mcode : ''}`) + return isNaN(mcode) ? undefined : mcode +} + +/** + * 외부 링크 이동 (새 창) + * + * @param {string} link - 이동할 외부 링크 + */ +const csrGoExternalLink = (link: string = '') => { + window.open(link, '_blank') +} + +/** + * QA용 국가 코드 조회 + */ +const csrGetQc = () => { + const route = useRoute() + const qc = `${route.query.qc != null && route.query.qc !== '' ? route.query.qc : ''}` + return qc +} + +/** + * 문자열이 숫자인지 확인 + * + * @param {string} str - 확인할 문자열 + */ +const isNumeric = (str: string): boolean => { + return /^-?\d+(\.\d+)?$/.test(str) +} + +/** + * 가공된 링크 파싱 + * + * @param {string} link - 원본 링크 + * @param {Function} tm - i18n의 tm 함수 (예: (key) => ({ txt: string })) + * @param {any} query - 추가 쿼리 파라미터 + */ +const getParsedCustomLink = (link: string, { tm, query = {} }: ParsedCustomLinkOptions) => { + const config = useRuntimeConfig() + let result = `${link || ''}` + + // @c{key} 패턴 치환 (예: @c{stoveCommunityUrl}) + if (link.includes('@c')) { + result = result.replace(/@c\{(.*?)\}/g, (_, key) => { + // config.public에서 해당 key 값을 찾아 치환 + return typeof config.public[key] === 'string' ? config.public[key] : '' + }) + } + + // @m{key} 패턴 치환 (예: @m{Community_Channel_Key}) + if (link.includes('@m')) { + result = result.replace(/@m\{(.*?)\}/g, (_, key) => { + // tm 함수로 변환하여 치환 + return tm(key)?.txt ?? '' + }) + } + + // @q{key} 패턴 치환 (예: @q{ppid}) + if (link.includes('@q')) { + result = result.replace(/@q\{(.*?)\}/g, (_, key) => { + let q = '' + if (query[key]) { + q += result.includes('?') ? '&' : '?' + q += `${key}=${query[key]}` + } + return q + }) + } + return result +} + +/** + * 쿠키 설정 - 만료기간 하루 단위 셋팅 + * + * @param {string} name - 쿠키 이름 + * @param {string} value - 쿠키 값 + * @param {number} exp - 만료기간 (옵션) + */ +const setCookieForDay = (name: string, value: string, exp?: number) => { + const date = new Date() + if (!exp) { + exp = 1 + } + date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000) + + const setCookie = useCookie(name, { + expires: new Date(date), + path: '/' + }) + + setCookie.value = value +} + +// 정적 파일인지 확인하는 함수 +const isStaticFile = (path: string): boolean => { + return /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|scss)$/i.test(path) +} + +/** + * 기준값이 최솟값 ~ 최댓값에 속하는지 확인 + * + * @param {number} ref - 기준값 + * @param {number} min - 최솟값 + * @param {number} max - 최댓값 + */ +const isInRange = (ref: number, min: number, max: number): boolean => { + return ref >= min && ref <= max +} + +export { + checkPageValidation, + csrDownloadFile, + csrGetMarketingCode, + csrGoExternalLink, + csrGetQc, + isNumeric, + getParsedCustomLink, + setCookieForDay, + isStaticFile, + isInRange +} diff --git a/layers/utils/dataUtil.ts b/layers/utils/dataUtil.ts index 7c3d66d..b9fa47b 100644 --- a/layers/utils/dataUtil.ts +++ b/layers/utils/dataUtil.ts @@ -154,3 +154,50 @@ export const getCurrentTimestamp = (unit: 'ms' | 's' = 'ms'): number => { const now = Date.now() return unit === 's' ? Math.floor(now / 1000) : now } + + +export const formatDateOffset = ({ + ts, + lang, + useSeconds, + useTimezone +}: { + ts: number + lang: string + useSeconds?: boolean + useTimezone?: boolean +}) => { + const offset = { ko: 9, ja: 9, 'zh-tw': 8, en: 0 }[lang] || 0 + const date = new Date(ts + offset * 3600000) + const pad = (n: number) => String(n).padStart(2, '0') + + const year = date.getUTCFullYear() + const month = date.getUTCMonth() + 1 + const day = date.getUTCDate() + const hours = date.getUTCHours() + const minutes = date.getUTCMinutes() + const seconds = date.getUTCSeconds() + + if (lang === 'ko') { + let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}` + format += useSeconds ? `:${pad(seconds)}` : '' + format += useTimezone ? ' (KST)' : '' + return `${format}` + } else if (lang === 'zh-tw') { + let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}` + format += useSeconds ? `:${pad(seconds)}` : '' + format += useTimezone ? ` (UTC${offset > 0 ? '+' + offset : ''})` : '' + return `${format}` + } else if (lang === 'ja') { + let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}` + format += useSeconds ? `:${pad(seconds)}` : '' + format += useTimezone ? ' (日本時間)' : '' + return `${format}` + } else { + //= en + let format = `${pad(month)}/${pad(day)}/${year} ${pad(hours)}:${pad(minutes)}` + format += useSeconds ? `:${pad(seconds)}` : '' + format += useTimezone ? ' (UTC)' : '' + return `${format}` + } +} diff --git a/package.json b/package.json index 6f980b3..b3ad0ff 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nuxtjs/device": "^3.2.4", "@nuxtjs/i18n": "^10.0.6", "@pinia/nuxt": "^0.6.1", + "@seed-next/date": "^0.0.0", "@splidejs/splide": "^4.1.4", "@splidejs/vue-splide": "^0.6.12", "@vueuse/core": "^13.6.0", @@ -55,6 +56,7 @@ "eslint-plugin-nuxt": "^4.0.0", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-vue": "^10.4.0", + "lru-cache": "^11.1.0", "postcss": "^8.5.6", "prettier": "^3.6.2", "tailwindcss": "^3.4.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92b9f42..336f762 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@pinia/nuxt': specifier: ^0.6.1 version: 0.6.1(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) + '@seed-next/date': + specifier: ^0.0.0 + version: 0.0.0 '@splidejs/splide': specifier: ^4.1.4 version: 4.1.4 @@ -96,6 +99,9 @@ importers: eslint-plugin-vue: specifier: ^10.4.0 version: 10.4.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.5.1))) + lru-cache: + specifier: ^11.1.0 + version: 11.2.2 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -106,7 +112,7 @@ importers: specifier: ^3.4.17 version: 3.4.17 typescript: - specifier: ^5.5.0 + specifier: ^5.3.3 version: 5.9.2 vue-tsc: specifier: ^3.0.7 @@ -302,6 +308,12 @@ packages: peerDependencies: postcss-selector-parser: ^7.0.0 + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + + '@date-fns/utc@2.1.1': + resolution: {integrity: sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==} + '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} @@ -1496,6 +1508,9 @@ packages: cpu: [x64] os: [win32] + '@seed-next/date@0.0.0': + resolution: {integrity: sha1-d6+dtjsFjxR4SGWSuBpJrxZCgwc=, tarball: https://git.sginfra.net/api/v4/projects/3708/packages/npm/@seed-next/date/-/@seed-next/date-0.0.0.tgz} + '@sindresorhus/is@7.0.2': resolution: {integrity: sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==} engines: {node: '>=18'} @@ -2288,6 +2303,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + db0@0.3.2: resolution: {integrity: sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==} peerDependencies: @@ -3246,6 +3264,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -5087,6 +5109,10 @@ snapshots: dependencies: postcss-selector-parser: 7.1.0 + '@date-fns/tz@1.4.1': {} + + '@date-fns/utc@2.1.1': {} + '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -6253,6 +6279,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.50.0': optional: true + '@seed-next/date@0.0.0': + dependencies: + '@date-fns/tz': 1.4.1 + '@date-fns/utc': 2.1.1 + date-fns: 4.1.0 + '@sindresorhus/is@7.0.2': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -7155,6 +7187,8 @@ snapshots: csstype@3.1.3: {} + date-fns@4.1.0: {} + db0@0.3.2: {} de-indent@1.0.2: {} @@ -8147,6 +8181,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 diff --git a/temp/middleware.ts b/temp/middleware.ts new file mode 100644 index 0000000..03cd7f3 --- /dev/null +++ b/temp/middleware.ts @@ -0,0 +1,286 @@ +import { LRUCache } from 'lru-cache' +// import { DEFAULT_LOCALE_COVERAGES } from '@/i18n.config' +import { getTrueClientIp } from '#layers/utils/apiUtil' +import { ssrGetFinalLocale } from '#layers/utils/localeUtil' +import type { ResGetInspectionData, WebInspectionData } from '#layers/types/InspectionType' +import { isStaticFile } from '#layers/utils/commonUtil' + + +console.log("🚀 ~ setCacheHeaders ~ event.node.res.setHeader:") +/** + * 캐시 제어 헤더를 설정하는 공통 함수 + * + * @param event - 이벤트 객체 + * @param cacheMode - 캐시 모드 설정 ('no-cache', 'short', 'medium', 'default') + * @param customMaxAge - 커스텀 max-age 값 (초 단위) + */ +function setCacheHeaders( + event: { node: { res: { setHeader: (name: string, value: string) => void } } }, + cacheMode: 'no-cache' | 'short' | 'medium' | 'default', + customMaxAge?: number +): void { + // 원래 setHeader 함수 참조 저장 + const originalSetHeader = event.node.res.setHeader + + // Cache-Control 헤더 설정값 결정 + let cacheControl: string + switch (cacheMode) { + case 'no-cache': + cacheControl = 'no-cache, no-store, must-revalidate' + // no-cache 모드일 때는 추가 헤더도 설정 + event.node.res.setHeader('Pragma', 'no-cache') + event.node.res.setHeader('Expires', '0') + break + case 'short': + cacheControl = `public, max-age=${customMaxAge || 10}` + break + case 'medium': + cacheControl = `public, max-age=${customMaxAge || 15}` + break + case 'default': + default: + cacheControl = `public, max-age=${customMaxAge || 60}` + break + } + + // Cache-Control 헤더를 강제로 설정하기 위해 setHeader 메소드 오버라이드 + event.node.res.setHeader = function (name: string, value: string) { + if (name.toLowerCase() === 'cache-control') { + return originalSetHeader.call(this, name, cacheControl) + } + return originalSetHeader.call(this, name, value) + } + + // 바로 캐시 제어 헤더 적용 +} + +const cache = new LRUCache({ + max: 100, // 캐시에 저장할 최대 항목 수 + ttl: 1000 * 30 // 30초 동안 캐시 유지 +}) + + +/** + * 최종 언어 쿠키 세팅 + * + * @param event - 이벤트 객체 + * @param finalLocale - 최종 언어 + * @param baseDomain - 기본 도메인 + */ +function setFinalLocaleCookie(event: any, finalLocale: string, baseDomain: string) { + setCookie(event, 'LOCALE', finalLocale.toUpperCase(), { + domain: baseDomain, + path: '/', + maxAge: 60 * 60 * 24 * 365 // 1년 (초 단위) + }) +} + +/** + * Locale Middleware 역할 함수 + * + * @param event - 이벤트 객체 + * @param finalLocale - 최종 언어 + */ +function fnLocaleMiddleware(event: any, finalLocale: string) { + const path = event?.node.req.url || '' + let arrPath = [] + let queryString = '' + + if (path.includes('?')) { + // 쿼리스트링 포함 시 순수 경로만 추출 + arrPath = path.split('?')[0].split('/') + queryString = path.split('?')[1] + } else { + arrPath = path.split('/') + queryString = '' + } + + // 최종 언어 세팅된 경로 생성 + const pathLocale = arrPath.length > 1 ? arrPath[1] : '' + + // URL에서 현재 언어와 최종 언어가 다르면 리다이렉트 + if (pathLocale !== finalLocale) { + let newLocalePath = '' + if (pathLocale === '') { + newLocalePath = `/${finalLocale}` + } else { + arrPath[1] = finalLocale + newLocalePath = arrPath.join('/') + } + + if (queryString !== '') { + newLocalePath += `?${queryString}` + } + + event.node.res.statusCode = 302 + event.node.res.setHeader('Location', newLocalePath) + event.node.res.end() + } +} + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const runType = `${config.public.runType}` + const iBaseApiUrl = `${config.public.stoveApiUrlServer}` + const gameId = `${event.context.gameData?.game_id}` + const baseDomain = `${config.public.baseDomain}` + + if (['local', 'local-gate8', 'dev'].includes(runType)) { + // Sandbox 이상 환경에서만 동작 및 확인 가능 (local, dev는 통과 처리) + try { + // 언어 코드 추출 + const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers) + setFinalLocaleCookie(event, finalLocale, baseDomain) + + // ------------------------------------------------------------------------------- + // [Locale Middleware] + // ------------------------------------------------------------------------------- + fnLocaleMiddleware(event, finalLocale) + } catch (e) { + console.error('[Exception] /server/middleware/middleware-global: ', e) + } + } else { + // ------------------------------------------------------------------------------- + // [Inspection Middleware] + // ------------------------------------------------------------------------------- + const fullPath = event.path + + // 1-1. 정적 파일 패스 + if (isStaticFile(event.path)) { + return + } + + // 1-2. /inspection 패스 + if (fullPath.includes('/inspection')) { + // 리턴 되기 전 언어 쿠키 세팅 + const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers) + setFinalLocaleCookie(event, finalLocale, baseDomain) + return + } + + // 1-3. 특정 경로 패스 (API, 리소스) + if ( + fullPath.startsWith('/api/') || + fullPath.startsWith('/_nuxt/') || + fullPath.includes('/assets/') || + fullPath.includes('favicon') + ) { + return + } + + // 캐시 키 생성 + const cacheKey = 'inspection' + + try { + // 2. 언어 코드 추출 + const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers) + setFinalLocaleCookie(event, finalLocale, baseDomain) + + // 초기화 + let inspectionData + + // 3. 캐시된 데이터가 없거나 만료되었을 때만 API 호출 + if (cache.has(cacheKey)) { + inspectionData = cache.get(cacheKey) as WebInspectionData + } else { + const apiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${gameId}` + // 직접 $fetch 사용 (composable 사용하지 않음) + const response = await $fetch (apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + inspectionData = response?.value?.inspection as WebInspectionData + console.log("🚀 00000 inspectionData:", inspectionData) + cache.set(cacheKey, inspectionData) // 캐시에 저장 + } + + // 4. 현재 시간과 점검 기간 비교 + const currentTime = Date.now() + const tsStartDate = inspectionData?.ts_start_date || 0 + const tsEndDate = inspectionData?.ts_end_date || 0 + const timeUntilInspectionSeconds = Math.floor((tsStartDate - currentTime) / 1000) + + // 5. 점검 상태별 캐시 설정 + if (inspectionData?.inspection_status === 1 && currentTime >= tsStartDate && currentTime <= tsEndDate) { + /** + * 점검 중인 경우 + * - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인 + * - 점검 URL 경로가 아닐 경우 no-cache 설정 + * - 화이트 리스트 체크 + */ + // 점검 url path 가 아닐 경우, no-cache 설정 + const inspectionPath = `/${finalLocale}/inspection` + if (fullPath !== inspectionPath) { + setCacheHeaders(event, 'no-cache') + } + + // 점검 중일 때 IP 필터링 활성화 여부 확인 + if (inspectionData?.ip_filter_use_yn === 'Y') { + const clientIP = getTrueClientIp(event.node.req) + + // 허용된 IP 목록 확인 + if (!inspectionData?.ip_filter_list?.includes(clientIP)) { + // 허용되지 않은 IP인 경우 점검 페이지로 이동 + event.node.res.statusCode = 302 + event.node.res.setHeader('Location', inspectionPath) + event.node.res.end() + } else { + // 화이트 리스트인 경우 + // ------------------------------------------------------------------------------- + // [Locale Middleware] + // ------------------------------------------------------------------------------- + fnLocaleMiddleware(event, finalLocale) + } + } else { + event.node.res.statusCode = 302 + event.node.res.setHeader('Location', inspectionPath) + event.node.res.end() + } + } else { + /** + * 점검이 아닌 경우 + * - 홈 경로는 no-cache + * - 점검 예정 시간에 따른 캐시 설정 + * - 점검 5분 전: 짧은 캐시 (10초) + * - 점검 30분 전: 중간 캐시 (15초) + * - 점검 30분 이후: 기본 캐시 (60초) + */ + // 홈 경로: 캐시 없음 + const isHomePath = [ + '', + '/' + //, ...Object.values(DEFAULT_LOCALE_COVERAGES).flatMap((locale) => [`/${locale}`, `/${locale}/`]) + ].includes(fullPath) + + if (isHomePath) { + setCacheHeaders(event, 'no-cache') + } else { + // 점검 예정 시간에 따른 캐시 설정 + + if (tsStartDate > 0 && timeUntilInspectionSeconds > 0) { + if (timeUntilInspectionSeconds < 300) { + // 점검 5분 전: 짧은 캐시 (10초) + setCacheHeaders(event, 'short', 10) + } else if (timeUntilInspectionSeconds < 1800) { + // 점검 30분 전: 중간 캐시 (15초) + setCacheHeaders(event, 'medium', 15) + } else { + // 점검 30분 이후: 기본 캐시 (60초) + setCacheHeaders(event, 'default') + } + } + } + // ------------------------------------------------------------------------------- + // [Locale Middleware] + // ------------------------------------------------------------------------------- + fnLocaleMiddleware(event, finalLocale) + } + + // 정상 접속 허용 + } catch (e) { + console.error('[Exception] /server/middleware/middleware-02-global: ', e) + } + } +}) diff --git a/tsconfig.json b/tsconfig.json index 51f3aea..698edbb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,6 @@ "types/**/*", "layers/**/*", "app/**/*" - ], +, "temp/inspection.ts", "temp/middleware.ts" ], "exclude": [".nuxt/types/**/*", "node_modules"] } From 5c43a3e838b2fc044653c9cf1c08ffa36f87d602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chyeonggkim=E2=80=9D?= <“hyeonggkim@smilegate.com”> Date: Thu, 30 Oct 2025 18:31:24 +0900 Subject: [PATCH 5/8] fix: disable TypeScript type checking and update inspection page layout --- app/pages/inspection/index.vue | 138 ++++++++++++++++++--------- layers/server/middleware/gameData.ts | 4 +- layers/types/DataizationType.ts | 3 +- layers/types/InspectionType.ts | 1 + layers/utils/commonUtil.ts | 2 +- nuxt.config.ts | 2 +- 6 files changed, 97 insertions(+), 53 deletions(-) diff --git a/app/pages/inspection/index.vue b/app/pages/inspection/index.vue index afc0268..44e1bfe 100644 --- a/app/pages/inspection/index.vue +++ b/app/pages/inspection/index.vue @@ -40,24 +40,31 @@ - -@@ -77,7 +94,7 @@ diff --git a/layers/server/middleware/gameData.ts b/layers/server/middleware/gameData.ts index 480b2cc..b94dac8 100644 --- a/layers/server/middleware/gameData.ts +++ b/layers/server/middleware/gameData.ts @@ -60,7 +60,7 @@ export default defineEventHandler(async event => { event.context.gameData = response.value event.context.googleAnalyticsId = response.value?.ga_code - // console.log('🚀 ~ gameData:', response.value) + console.log('🚀 ~ gameData:', response.value) // 점검 데이터 조회 if (response.value.game_id) { @@ -76,7 +76,7 @@ export default defineEventHandler(async event => { inspectionData = inspectionResponse?.value?.inspection // console.log("🚀 ~ inspectionData:", inspectionData) - if (inspectionData?.inspection_status === 0 ) { + if (inspectionData?.inspection_status === 0) { /** * 점검 중인 경우 * - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ diff --git a/layers/types/DataizationType.ts b/layers/types/DataizationType.ts index 07faafd..a667182 100644 --- a/layers/types/DataizationType.ts +++ b/layers/types/DataizationType.ts @@ -1,5 +1,4 @@ -import type { PromotionPreregistType } from '@/types/promotion/PreregistType' -import type { CommonPeriodType } from '@/types/CommonType' +import type { CommonPeriodType } from '#layers/types/Common' // [S] Type in czn_homepage_brand_siteConfig.json ---------------------------------------- interface GnbMenuType { diff --git a/layers/types/InspectionType.ts b/layers/types/InspectionType.ts index 69ed9ae..846c84d 100644 --- a/layers/types/InspectionType.ts +++ b/layers/types/InspectionType.ts @@ -21,6 +21,7 @@ interface WebInspectionData { // Internal ----- ip_filter_use_yn?: string // IP 필터 사용 여부 ("Y" 또는 "N") ip_filter_list?: string[] // 허용된 IP 목록 + launching_status?: number // 런칭 여부 (0: 런칭 전, 1: 런칭 후) } interface ReqGetInspectionData extends CommonRequestType { diff --git a/layers/utils/commonUtil.ts b/layers/utils/commonUtil.ts index 1d22531..fb42881 100644 --- a/layers/utils/commonUtil.ts +++ b/layers/utils/commonUtil.ts @@ -1,4 +1,4 @@ -import type { ParsedCustomLinkOptions } from '@/types/CommonType' +import type { ParsedCustomLinkOptions } from '#layers/types/Common' /** * 페이지 - 유효성 체크 diff --git a/nuxt.config.ts b/nuxt.config.ts index 29d7b5f..02de4e9 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -63,7 +63,7 @@ export default defineNuxtConfig({ payloadExtraction: false, }, typescript: { - typeCheck: true, + typeCheck: false, strict: false, }, nitro: { From 74851e01ed9bdcbca49cf869cac8cb4831cbfff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chyeonggkim=E2=80=9D?= <“hyeonggkim@smilegate.com”> Date: Fri, 31 Oct 2025 15:30:06 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EC=A0=90=EA=B2=80=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80,=20=EC=96=B8=EC=96=B4?= =?UTF-8?q?=20=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pages/[d1]/[d2]/[d3].vue | 1 + app/pages/[d1]/[d2]/index.vue | 1 + app/pages/[d1]/index.vue | 1 + app/pages/index.vue | 2 + app/pages/inspection/index.vue | 272 ++++++++-------- layers/components/blocks/LanguageSwitcher.vue | 43 ++- layers/components/layouts/Footer.vue | 91 ++---- layers/middleware/inspection.ts | 21 +- layers/middleware/pageData.global.ts | 14 +- layers/server/middleware/gameData.ts | 302 ++++++++++++++++-- layers/server/plugins/nitroPlugin.ts | 68 ++++ layers/types/Common.ts | 31 +- layers/utils/localeUtil.ts | 138 ++++---- 13 files changed, 629 insertions(+), 356 deletions(-) create mode 100644 layers/server/plugins/nitroPlugin.ts diff --git a/app/pages/[d1]/[d2]/[d3].vue b/app/pages/[d1]/[d2]/[d3].vue index dceae28..6d1ae69 100644 --- a/app/pages/[d1]/[d2]/[d3].vue +++ b/app/pages/[d1]/[d2]/[d3].vue @@ -9,6 +9,7 @@ const currentLayout = computed(() => getLayoutType(pageData.value)) definePageMeta({ layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화 + middleware: ['inspection'] }) diff --git a/app/pages/[d1]/[d2]/index.vue b/app/pages/[d1]/[d2]/index.vue index dceae28..6d1ae69 100644 --- a/app/pages/[d1]/[d2]/index.vue +++ b/app/pages/[d1]/[d2]/index.vue @@ -9,6 +9,7 @@ const currentLayout = computed(() => getLayoutType(pageData.value)) definePageMeta({ layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화 + middleware: ['inspection'] }) diff --git a/app/pages/[d1]/index.vue b/app/pages/[d1]/index.vue index dceae28..6d1ae69 100644 --- a/app/pages/[d1]/index.vue +++ b/app/pages/[d1]/index.vue @@ -9,6 +9,7 @@ const currentLayout = computed(() => getLayoutType(pageData.value)) definePageMeta({ layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화 + middleware: ['inspection'] }) diff --git a/app/pages/index.vue b/app/pages/index.vue index 43db7c4..6db7172 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -11,7 +11,9 @@ console.log("🚀 ~ currentLayout:", currentLayout) definePageMeta({ layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화 + middleware: ['inspection'] }) + diff --git a/app/pages/inspection/index.vue b/app/pages/inspection/index.vue index 44e1bfe..ed89d61 100644 --- a/app/pages/inspection/index.vue +++ b/app/pages/inspection/index.vue @@ -1,101 +1,107 @@+ +@@ -66,8 +73,18 @@{{ tm('Inspection_Community_Btn') || '공식 커뮤니티' }}- - {{ tm('game_start_btn') || '게임 실행' }} - + 게임 시작 + +- + + ++ {{ tm('Inspection_Txt_Download') || '게임 다운로드' }}
-- {{ webInspectionData.inspection_content }} ++ ++ {{ getButtonText(btn.platform) }} + +- - -+ +-
+ + +-+
- -- - {{ webInspectionData.inspection_title1 }} - - +
+ + +{{ tm('Inspection_Now_Maintenance') }} - -
+ -- --- - -{{ tm('Inspection_Maintenance_Time') }}
---- {{ getLocaleTimezone(locale) }} --- {{ getLocaleTimezone('en', 'US') }} --- {{ getLocaleTimezone('zh-tw', '') }} --- {{ getLocaleTimezone('ja', '') }} --- -- -- - {{ tm('Inspection_Community_Btn') || '공식 커뮤니티' }} -- - - 게임 시작 - - +- - -- + ++ + diff --git a/app/pages/error.vue b/app/pages/error.vue new file mode 100644 index 0000000..6de5f13 --- /dev/null +++ b/app/pages/error.vue @@ -0,0 +1,95 @@ + ++- -{{ tm('Inspection_Maintenance_Time') }}
++ + + +--- {{ tm('Inspection_Txt_Download') || '게임 다운로드' }} -
-- -+ + +++ + ++ + +
++ + +++ {{ tm('Inspection_Community_Btn') }} + + ++ + + 게임 시작 + + ++ ++ ++ {{ tm('Inspection_Txt_Download') || '게임 다운로드' }} +
+++ {{ getButtonText(btn.platform) }} + +++ + + + \ No newline at end of file diff --git a/app/pages/index.vue b/app/pages/index.vue index 6db7172..a151a5c 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -7,13 +7,10 @@ const { pageData } = storeToRefs(pageDataStore) const currentLayout = computed(() => getLayoutType(pageData.value)) -console.log("🚀 ~ currentLayout:", currentLayout) definePageMeta({ layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화 - middleware: ['inspection'] }) - diff --git a/error.vue b/error.vue index 4055e3d..2509015 100644 --- a/error.vue +++ b/error.vue @@ -1,25 +1,95 @@ + ++ +++ ++++ + ++
+ ++ + +++ + ++
+++ {{ errorTitle }} +
++ {{ errorDescription }} +
++ {{ homeButtonText }} + +++ + +const gameDataStore = useGameDataStore() +const { gameData } = storeToRefs(gameDataStore) +const localePath = useLocalePath() - -+ +++ ++++ + ++
+ ++ + +++ + ++
+++ {{ errorTitle }} +
++ {{ errorDescription }} +
++ {{ homeButtonText }} + +-- +const gameName = computed(() => gameData.value?.game_name || '게임') +const homeButtonText = computed(() => `${gameName.value} 홈페이지 가기`) - + +// 에러 상태 코드에 따른 메시지 설정 +const errorTitle = computed(() => { + if (props.error?.statusCode === 404) { + return '페이지를 찾을 수 없어요.' + } + return '페이지를 찾을 수 없어요.' +}) + +const errorDescription = computed(() => { + if (props.error?.statusCode === 404) { + return '주소가 바뀌었거나 잘못 입력된 것 같아요. 주소를 다시 확인해 주세요.' + } + return '주소가 바뀌었거나 잘못 입력된 것 같아요. 주소를 다시 확인해 주세요.' +}) + +definePageMeta({ + layout: 'only-stove' +}) + + + + \ No newline at end of file diff --git a/layers/components/blocks/StoveGnbNew.vue b/layers/components/blocks/StoveGnbNew.vue new file mode 100644 index 0000000..996902d --- /dev/null +++ b/layers/components/blocks/StoveGnbNew.vue @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/layers/components/layouts/Footer.vue b/layers/components/layouts/Footer.vue index 60a51bd..936d456 100644 --- a/layers/components/layouts/Footer.vue +++ b/layers/components/layouts/Footer.vue @@ -56,7 +56,7 @@{{ error?.statusCode }}
-{{ error?.statusMessage }}
-- Clear errors - -{{ footerAgeRatingInfo[0] }} - {{ footerData.game_rating_info.company_name }} + {{ footerData.game_rating_info.title }}@@ -158,8 +158,7 @@ const { tm } = useI18n({ const gameDataStore = useGameDataStore() const { gameData } = storeToRefs(gameDataStore) - -const path = ref(`${staticUrl}/local/template/${gameData.value.s3_folder_name}`) +// const path = ref (`${staticUrl}/local/template/${gameData.value.s3_folder_name}`) // 공통다국어 data const footerLinks = computed((): FooterMenuItem[] => { @@ -179,17 +178,17 @@ const getGameRatingImage = computed((): string[] => { return contentInfo.map(item => { switch (item) { case '12': - return `${path.value}/common/grades_age/Type12.svg` + return getImageHost('/images/common/grades_age/Type12.svg', { imageType: 'common' }) case '15': - return `${path.value}/common/grades_age/Type15.svg` + return getImageHost('/images/common/grades_age/Type15.svg', { imageType: 'common' }) case '19': - return `${path.value}/common/grades_age/Type19.svg` + return getImageHost('/images/common/grades_age/Type19.svg', { imageType: 'common' }) case 'all': - return `${path.value}/common/grades_age/TypeAll.svg` + return getImageHost('/images/common/grades_age/TypeAll.svg', { imageType: 'common' }) case 'e': - return `${path.value}/common/grades_age/TypeExempt.svg` + return getImageHost('/images/common/grades_age/TypeExempt.svg', { imageType: 'common' }) default: - return `${path.value}/common/grades_age/TypeTest.svg` + return getImageHost('/images/common/grades_age/TypeTest.svg', { imageType: 'common' }) } }) }) @@ -201,19 +200,19 @@ const getContentInfoImage = computed((): string[] => { return contentInfo.map(item => { switch (item) { case '1': - return `${path.value}/common/grades_use/Type-sexual.svg` + return getImageHost('/images/common/grades_use/Type-sexual.svg', { imageType: 'common' }) case '2': - return `${path.value}/common/grades_use/Type-fear.svg` + return getImageHost('/images/common/grades_use/Type-fear.svg', { imageType: 'common' }) case '3': - return `${path.value}/common/grades_use/Type-inapposite.svg` + return getImageHost('/images/common/grades_use/Type-inapposite.svg', { imageType: 'common' }) case '4': - return `${path.value}/common/grades_use/Type-drug.svg` + return getImageHost('/images/common/grades_use/Type-drug.svg', { imageType: 'common' }) case '5': - return `${path.value}/common/grades_use/Type-crime.svg` + return getImageHost('/images/common/grades_use/Type-crime.svg', { imageType: 'common' }) case '6': - return `${path.value}/common/grades_use/Type-speculation.svg` + return getImageHost('/images/common/grades_use/Type-speculation.svg', { imageType: 'common' }) case '7': - return `${path.value}/common/grades_use/Type-violence.svg` + return getImageHost('/images/common/grades_use/Type-violence.svg', { imageType: 'common' }) } }) }) diff --git a/layers/components/layouts/Header.vue b/layers/components/layouts/Header.vue index ef03c13..3fa922d 100644 --- a/layers/components/layouts/Header.vue +++ b/layers/components/layouts/Header.vue @@ -171,7 +171,7 @@ onBeforeUnmount(() => { - + diff --git a/layers/components/layouts/StoveHeader.vue b/layers/components/layouts/StoveHeader.vue new file mode 100644 index 0000000..ae4abe8 --- /dev/null +++ b/layers/components/layouts/StoveHeader.vue @@ -0,0 +1,18 @@ + + + + + + + + +c \ No newline at end of file diff --git a/layers/layouts/inspection.vue b/layers/layouts/onlyStove.vue similarity index 73% rename from layers/layouts/inspection.vue rename to layers/layouts/onlyStove.vue index ddd7e8c..33b1bcd 100644 --- a/layers/layouts/inspection.vue +++ b/layers/layouts/onlyStove.vue @@ -1,6 +1,6 @@ -+ + + diff --git a/layers/server/middleware/gameData.ts b/layers/server/middleware/gameData.ts index a5a251e..1998582 100644 --- a/layers/server/middleware/gameData.ts +++ b/layers/server/middleware/gameData.ts @@ -203,6 +203,7 @@ export default defineEventHandler(async event => { game_domain: event.context.gameDomain || '', lang_code: finalLocale, } + console.log("🚀 ~ apiUrl:", queryParams) const response = (await $fetch(apiUrl, { query: queryParams, })) as GameDataResponse | null diff --git a/layers/types/Common.ts b/layers/types/Common.ts index 70c472a..5b60274 100644 --- a/layers/types/Common.ts +++ b/layers/types/Common.ts @@ -37,6 +37,7 @@ interface FooterMenuItem { } interface GameRatingInfo { + title: string company_name: string rating_grade: string reg_no: string @@ -47,8 +48,13 @@ interface GameRatingInfo { } interface FooterData { + use_game_rating: boolean game_rating_info: GameRatingInfo - use_dev_ci_url?: string + use_dev_ci_url?: boolean + dev_ci_url?: string + dev_ci_img_path?: string + fund_display_yn?: string + fund_display_url?: string } interface DevCiConfig { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 336f762..b3f4e6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,7 +112,7 @@ importers: specifier: ^3.4.17 version: 3.4.17 typescript: - specifier: ^5.3.3 + specifier: ^5.5.0 version: 5.9.2 vue-tsc: specifier: ^3.0.7 From 33aece10112af8c6f3a91ad5b6f0a381bd3399eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chyeonggkim=E2=80=9D?= <“hyeonggkim@smilegate.com”> Date: Mon, 3 Nov 2025 13:06:58 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EC=A0=90=EA=B2=80=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80,=20external=20gamedata?= =?UTF-8?q?=20=ED=98=B8=EC=B6=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pages/inspection/index.vue | 45 ++++++++++------ layers/components/blocks/LanguageSwitcher.vue | 4 +- layers/composables/useGetGameDataExternal.ts | 54 +++++++++++++++++++ .../useGetInspectionDataExternal.ts | 1 + layers/middleware/init.route.global.ts | 24 +++++---- layers/middleware/inspection.ts | 3 +- layers/server/middleware/gameData.ts | 26 +++------ layers/types/api/gameData.ts | 4 ++ 8 files changed, 114 insertions(+), 47 deletions(-) create mode 100644 layers/composables/useGetGameDataExternal.ts diff --git a/app/pages/inspection/index.vue b/app/pages/inspection/index.vue index 72ba104..f3ba325 100644 --- a/app/pages/inspection/index.vue +++ b/app/pages/inspection/index.vue @@ -1,6 +1,9 @@ + + + + - @@ -19,10 +22,7 @@@@ -109,7 +109,7 @@ const rootPath = config.public.staticUrl const runType = config.public.runType const translationApi = `${rootPath}/${runType}/test` -const isClient = import.meta.client +// const isClient = import.meta.client const inspectionStore = useInspectionStore() const { webInspectionData } = storeToRefs(inspectionStore) @@ -133,19 +133,32 @@ const { tm, locale } = useI18n({ // ja: (JST) // 나머지: (KST) const getLocaleTimezone = (localeType: string, region) => { - const tsStartDate = webInspectionData.value?.start_date || 0 - const tsEndDate = webInspectionData.value?.end_date || 0 - switch (localeType) { + const tsStartDate = webInspectionData.value?.ts_start_date || 0 + const tsEndDate = webInspectionData.value?.ts_end_date || 0 + const currentLocale = localeType ? localeType : locale.value + switch (currentLocale) { case 'ko': - return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})}{{ tm('Inspection_Maintenance_Time') }}
- - - - +
~ ${globalDateFormat(new Date(tsEndDate), localeType, region || 'KR', {useFullDate: true})} (KST)` + return ` + ${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || 'KR', {useFullDate: true})} (KST)
+ ${globalDateFormat(new Date(tsStartDate), 'en', region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), 'en', region || '', {useFullDate: true})} (UTC) + ` case 'en': - return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (UTC)` + return `${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || '', {useFullDate: true})} (UTC)` case 'zh-tw': - return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (台灣時間)` + return ` + ${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || '', {useFullDate: true})} (台灣時間)
+ ${globalDateFormat(new Date(tsStartDate), 'en', region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), 'en', region || '', {useFullDate: true})} (UTC) + ` case 'ja': - return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (JST)` + return ` + ${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || '', {useFullDate: true})} (JST)
+ ${globalDateFormat(new Date(tsStartDate), 'en', region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), 'en', region || '', {useFullDate: true})} (UTC) + ` default: - return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (KST)` + return ` + ${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || '', {useFullDate: true})} (KST)
+ ${globalDateFormat(new Date(tsStartDate), 'en', region || '', {useFullDate: true})}
~ ${globalDateFormat(new Date(tsEndDate), 'en', region || '', {useFullDate: true})} (UTC) + ` } } @@ -222,7 +235,7 @@ const handleGameStart = () => { definePageMeta({ middleware: ['inspection'], - layout: 'inspection', + layout: 'only-stove', showLoading: false }) @@ -230,7 +243,7 @@ definePageMeta({