From 6320088f32665f5508e39d7287d6c0d38ed1751a Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 01:10:44 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20kNN=20vocabulary=EB=A5=BC=20?= =?UTF-8?q?=EC=8B=9C=EB=93=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=20=EB=B0=A9=EC=8B=9D=EC=97=90=EC=84=9C=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20=EC=9E=90=EB=AA=A8/=EB=AC=B8=EC=9E=90=20=EC=A7=91?= =?UTF-8?q?=ED=95=A9=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 --- MLTraining/train.py | 19 ++++--- .../Resource/CategoryClassifier.mlmodel | Bin 188270 -> 280273 bytes .../SpendLearning/Resource/vocabulary.json | 48 +++++++++++++++++- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/MLTraining/train.py b/MLTraining/train.py index c8871c2..ce716db 100644 --- a/MLTraining/train.py +++ b/MLTraining/train.py @@ -19,6 +19,12 @@ HANGUL_BASE = 0xAC00 HANGUL_LAST = 0xD7A3 +# 한글 외 문자: 숫자, 영문 대소문자, 상호명에 흔히 쓰이는 특수문자 +DIGITS = list("0123456789") +ALPHA_LOWER = list("abcdefghijklmnopqrstuvwxyz") +ALPHA_UPPER = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ") +MISC_CHARS = [" ", "&", "+", "-", ".", ",", "'"] + def decompose_hangul(char): """완성형 한글 한 글자를 (초성, 중성, 종성) 튜플로 분해한다. 한글이 아니면 None.""" @@ -48,12 +54,11 @@ def tokenize(text): return tokens -def build_vocabulary(memos): - """시드 데이터에 등장하는 모든 자모/문자를 모아 정렬된 vocabulary를 만든다.""" - tokens = set() - for memo in memos: - tokens.update(tokenize(memo)) - return sorted(tokens) +def build_vocabulary(): + """이론상 가능한 전체 자모와 비한글 문자를 고정된 vocabulary로 만든다.""" + jongsung_tokens = ["_" + j for j in JONGSUNG if j] # 종성 없음("") 제외 + tokens = CHOSUNG + JUNGSUNG + jongsung_tokens + DIGITS + ALPHA_LOWER + ALPHA_UPPER + MISC_CHARS + return sorted(set(tokens)) def vectorize(memo, vocabulary): @@ -69,7 +74,7 @@ def main(): memos = [item["memo"] for item in seed_data] labels = [item["category"] for item in seed_data] - vocabulary = build_vocabulary(memos) + vocabulary = build_vocabulary() print(f"Vocabulary 크기: {len(vocabulary)}개 (자모/문자 단위)") vectors = [vectorize(memo, vocabulary) for memo in memos] diff --git a/SpendLearning/SpendLearning/Resource/CategoryClassifier.mlmodel b/SpendLearning/SpendLearning/Resource/CategoryClassifier.mlmodel index 84558c7eec8cc73623be0d2c06f0a51cc1c5092d..3c78442b204d37b00a36279c4a20e8adc579eec7 100644 GIT binary patch literal 280273 zcmeHwO|LB5k=60nI-Z$YFP?1?{=i0Gje`RG>W#p-n&WW!Tv17&FCo`+g zx$k}W*)RQ{PrdmYZ$9_i-~Qcy_D{e3=I?y?=F{K*)E7T```NGk)i4p+xvO#a<_Q1~iMhar zgnv|q;PI5)%34op>w3EWsIB`ETl=at!yvv|OoARyNt2XAKOeEs$>(OJL`#Vl#(nH%Rro|C)NKLck*SMnn z`c6qG&f@R^>^5Z5F!Q?TCMHQ}8%quEcbtVuo3#f7^I=_jhb@(Uv|FLSQNf!#%q3L4CjOcW+zGEGJRL&^a|y9-Kp*0go?>4iZ6W$o z_X~bfN^$Miwk`)dgjw_X7mZ-CR_ZBfdu`pl^3Wfoti$>i>sITz|Lk>(3E^jPlUh3z zksah+WZha$$F<#)lVaRIm!xz~W%cYX2s4&-#96lDjGgu8DamJYWDj;2a<5*w{_A^s zwWi7wvEujkaX%%UdtwaxM|7)3^mRX%AOmuwgI%#<*Xj=~9%l`XkgBVQyZW^)Zp)|2 z(!r6uf3Dn8>e?xZ1^BsJw#l{kujEsiXP-VPf2k^4-QN_6v|Ct z2g`^w(!%Sw*o*JA-EkJ?G{r#r0aUX`=_~X#r}MSlS7kvxuO-VYj;ZM#*0yvFA0+#< zE_Uw=^`57scVhR#VP|+f%V))bgJcYTzstUU{k7e3`X}=kq~2j|TU`O8C9v?rwqySi1<5jA`ISa<) zDY2Q%-h~;XNDrhRT-97)%lj4RLLt*Haei@}{Y^d02%mrVDy&yCzYxDAw>jC30Nf+% zQeJHBt1YXs+OFkJG53EtFwEE($rd$?vYWW1H||f{vIxhgMZ@$xe$#Pn3()e#0xxH~ z<1EblYI?_dZNbTx{sayFwG(;1$5}hMJ@arXAX{riT(}}3DKTQm*HV#l!c$^Qh^GPO zEg#44p<8`nUBn=Jf9t37OSbr9ZGgyk!c#&lTdFmgR?gmW>52O)>ADZb>6kuMRV*a~ z=kt`Hdm?m`x^=yqnzTzz5?(>xNFdM<}L`U+r#BvYNAzSMJ zrzPhhCO*~~qjliUcS^uaGlt!Y1NTTd++&!#MIIzLv~1u?(&s5*)~uIUvS09{Ih?0>OU-Gg%wY*wvS($5i{8WuuvMvm@&8)rX z%t3Uv!BP8|o0X&EY}fR;md8_4V|BKebsAbeE52qfsB^wMPS-R-7wWDc?1;6f6|;QE zt;K-RvX--)k{U`^d~G(*ChB3H0&Ga}vOb~0^nLo4#g@>4HIH{3o;2$R9`jADSsX22 z?;=*N|8k9`HZn;5sl@zijbb696gNq^m^1XCHE8uGT3wH|6Q>7f7njIqu&Hui2?JImHQX z6hKdqy=4X8lheXfyB*EpH;*1^^$r%z4lIOZ(MB`YcZ>@7QPkFYr4Odgp||x z1p1!-(p9#%tiHvEFIkZ>tY_VX%riNvIVvT=x_mBS2COThtIa)Xjx>YyA!BZcT2YyD zTDVqRRd;&FA(ncQ+E#=QZ)W`yS2SNc2@=B>@OXO7YDYkNog(+7Iz1(t#-OdIrR~9T z+rqJ@$}wz0wbrSx3jHw zxu;GKhWj4HbM8@e;b8Hwe9|-gkRv?ioIOs-NEbI_Lt0_YB+?4Uu_Bi%N7#@Dk2$bJ@is zzI2~#vo5`}&HBVEOy5_3^Z#6uO{8NMI!D_bC3b`8UbKycOa5BCm4jR@pQl77vUYlU zntRyA-a@7COU!x|Ygu6O`8*|S*@C#v*EVqXl*2uQjk~MV^La{co=;^WH`m^wfOUf4 zgXCo`-$W4} zF8zreePNr9QSWu063h@&fbB_Iy^9ZD%*DcTyrsYHQ_`BwBjS3OOFL}&;e#ypABqt> z;PNjTZ7S#V@{X5uVJx%y3adK zZ3?v**!N(7J%Tj*4du>EJi%LWf=;T>Q=(an*fOEfDUHzd~~ zC9Mfe@u27Mc)Qj2)FJg<#S@O1RdX1q^z)q(Mev+vNUsOk?qA~a%KWo?`1|veY&p&0 z@pmMYb{c+2=27~W7FdpR;HyiIe_z5ZS+6r)qHA)N?E7oz7g9Q4eT$B&^-lLrxXN~m zao(W@I6z`aZNqO##Mb*XI?&hk|}H$A?`$yuD+ zgI$FjWqW(x(N2jl!q@0Dp7^~c@Q72=bG9``+k4I9S=XAtBTk9c$I~}YK>svoyELa7 zt`;lROZ=z%R4rk5Lc`i|?bEg%Lo2TFFE1XiCF3k}-beG)IK^04*LBQCdS{&_&*Lcp z*{$R(Xm3i?E8yJyukIX!jPVst;+)_+6K@u-J}cMeci#&8ee zx+ElZkkv9U1fSF_hX5xyCB#P58e0W*VG18`(y~MVIK;ILe^Uk^&h}igY9etOn9BVn z_sx2%6O9$y@+EabVs#kaPf6<@+%bY|sy#V-Vv4&Q`c^IGSJHJ3L(g}|VRqem=tk}a ztnHGyXrX7RW7T3G>!@w{5cByr^h@{lL^$51hc48vAl@a`hZhwtm#W{*zvn~%YY{bL?*CTJWM$EJ=3DmUN1T#Ym#sLn8o_d&2fde@;26ug@YL~f?vFSn<6ZV= z=n2&_frYGL9}z=ekfk0Fhm?21`W0h3&|&+0dS%9}cMv`-q=yGEkTo)lfeu;AvtzIM zT4GP+cwW7`+Sb>OEPkP6`C3eoGrw*ZC^K2&%iJ@zjo(u&GUIJscrb$iPUO7I%aW#4-!3!ZTS#iC7;)lRbzWh5_TU_ zcYzZEspF$I);Zs`WONdO$28XD9w7()YdO*za_t=NpO@6x*_=Ij6Mmtu^CYJ)rTUX{ zzH7 zDN8-FpLOX~+a+HwuYpq%dTZVnXHDe39c;5&jYlnJt*-IfwsPt|?@zCq-R%?674K_v z|EzNt+OKWKT6zy&Fu)C5ORVLdzRf3qov9Vpn3i3u)fGG4r)qj)T~jzH2wWgzn;~l5 zN>-yIGv>HIC6P%7YYjmzq>kUy<{nCVsUOBqn2;kgOb*FBOOD6qlBN6Q-kDK6p#!`j zM=2r?vOnMBDPeZwj5*tV`Tsfd7u9l(z2S>li>@7ZzC7?!um$#Hz_mPm&Uj3@g}`rOY}rW zCU8sc$U37~^aZK&OMZ=GJIM(g>CwHPk{*ukFv0E^I}K^0F5W_Z5nl*k9sf_S1%jt= zW0x#4wb)jtWLcN5CDoOelvSEl+w!d{{!H3=0(?q#Ug}xCHi1wJ8nFg`lb^ur8vgG% zn@q#lgh#r-Nz0L*kUNs2v8eI-P6?RvL@Xc2AE}$Y1-m1S)wZ2=^%iPM)c+~Da(rDP zq;&OZ(6*p0Us|PX`9CGMurJT$aS`DIPFjw1g@yL;0v{MBDcTla#|!md-2Z(^2U*vUxKqTU=9^-Hz&EH^$NwEi4*|@y{|t8I z7IHQci}6diq)R5p@=D|h(7oSrx*3D;QDxkbv_avwIY+ftdyVHQx$KIXTDnj7u}UYw z@>@RXvg8K?#3A9IWJoM!ob8mD6Clo3-11S%T~zJ?IwTr^J;?yCA$3gIsKc3-5Am_* z5>$djm8ZPV1Tc5VHLH%~E?AG(lBN6cIdLxQR4K2hq z9&yi8(mAm^!Kyob-L(>?cP}`~r5hhcshsUv(!;-dnmxsif(c6_7C+0kWVAHzr=-R2OmXZq zq~-=cq>jUG#dDnZQ_{;A%sohOg48{i{EX|tFy7Kp!{_HE7NfKHWM{BDB~Gn)&u=fA zHIl`j*OK*YsO_2+(>}IEXS?Ur)M+0MLsFw2DQt9A7j>L;kxan~A`B7&R0&UQ-HOsIEit=c!m!&tV}Gj`Rw zmS}v-PP=(+rzBzs7ajVMd1M zzqIHc*<*mb^Q`xDGm0bi(q{!&b^wb)c^murb$Roj|H z&AuhKR`(ejnZRp)dOhL^l%CKR>$0ctxAxb>g(b_3FX2qv=Z-Vlg|xLMC!@2{@yNA% zS|06h6yNUQHD60Go$aTJ%HxuIfezVQP4KmxTFhR<<0%&i4x5P>Oh(R{{4mqMb zmr|D;kEeu)Wa`2!3zZ$A9C~B~Tp+m@%ZGR^CYV+nbs+b7E;TVeJSE@`**!HR>>$wt zWOa#9jkbe@{LHr9?NRwdFjv-@iRHI^$f?B^%-OCbVCGq3=d=VT_+eX;F`~0= z;Vs{i`Rh&UQ*PQ#~CkZuxZHBWNUMz zOEf!e7sqn2fYrD5X&V&ahlIZ*dvW;7N&oMJvq^-^({%JstyyR%K&?NC@t3rK-Q{*_ zH3V0Wd%u?OBzpAvG(!&oJk&__iKq}t8JTg>RpqQR@m0!eptsUmJhKWY2}O0^3V0WpOT)bFs3{SMjb;h z^{SscGF({EeMz>zm{l*3o{+@X_xr0pLL66E8fcS zc`aeqtXpio_(ph)V444D-hw~dwS=eAt1r{&&Db6{(t}`Yc*G!kx~*e9o{~DFo|&YW zt?Q-L`>fF(fQRH&EFZ^BJ(%9B)8i?T)>loUHi(17YP@=58mH}1sy?4fMp-i6oLFFl z51n~HX9;}^HYYeG4tIF%&b4;F7MEBpUhm249jC-<{Uk~iefYr}Qg_XM)-}$y&vr^| zmOj0;-PAE{*BJL=f%AXI0h8KP+{Z%7K>xNcjm{R&MRc|;eARclzk9uD8r4zVC)m?} zYu8Z&Qg>eRYusW#?>NgW9;xY_({|EZNG#LfeCUv7LdEI-j#DAm9a=lR?gcgia-St# zmg3kgOQX*7luT0$x6JZ>xy!EAPXchdvyxwMwb=9j$Tm$fOy3XpO?J-OJ?+mh8}W&C zT|T|W?l;=kt?R96YqSTAPwW+^B+1VRJfB|epVw?qcXHHri({-yYizTQwk;oGC5fEZ z_gn(zJQ>T!@q6gC9&}9Gx<4y!`E*{%_IXNbjBByd6prfGTwp_%`o1-;>-sz;%%U|B z#+LLgYLse^`nebAkf_yTYYcKH84{~$9PgB9cG_-IS^>Dn8Y|k0DWm<3;^RU^<@xkl z9L>W5dY83rHI`t4g&6lOHP3cR7G~|K01Mg9cnreokA2X!ZuqL7< zKS7SIhDv*t81oig+m)*Akjqci+O*0Do*qz^53xteYF}aQOk>3E@o8QXujBFQU1?hSV#M`gK0rQ>_5cJx&SM+CyfOiOlv`?S3^gUbJ*620tXFBx)-T z@PhMxN?rmu^~r^KAO2n_?u&J6{Zd@~h(Y$`7qJ1^-cL!9AqXnTc-9r{?k0MHtTqtx zXf01-t$1E>$?^YAxW>N5+=?B=r#Wf6)aQf?bCh#fpQ2aHjI_U}Ij{Lzg88Hqf%m?$ zrw3plxkJmxan?0f+rYyAm}?1EM>krw;u_C(E4LNrT94Lydlg(08x}TA>;k=N-mfJY zp;zG0yWDe1bk5M2&i8wqFvS1RV7;@^zHp2>F|d3R7Jn&5>4`ew!|Zy@9VcmLg~5zj zpGt)l1rLy-d1yOnkvK9AIwa??kM)S5n(!?++;J8UF#AcY1!=8Ri&=R1*5)DqXj1rS zPTJNz-x|;B;gslzTRXVzJZl^$MDMz`IT1PR6FtcZI$F2wJ5ExuUSQYgnm*R#>`LOs z;=(866zWAYyn?sl0%weUN>tIO`Jf5#PDu;V4rxR1LyrE|BCSRi*179@oXCP2fvRJ$ zA%VM9Ze8!zevm)X*9aE%YqC^t@t^Lwq&ALAY+>F2-Z?zd*V|w>hZ>MJYW6aQbyamn$2N^0y&%yo{ot-MG1q&M_eyWRjJeXmc|PIWwJ+nMK0I= zU_+wkmMu8Ou&!gM$^OSYm*_6JGw3}SW`evC*}KL_&0DcOJy{sbhh8LBivw4Z$2=t? zj7G4fDICrp=8ht_t3A?Z%r%l~F@`Un&)nmVV|8WuxWisu*U@%wog?DF1(JKTd=j_( z;$uJST(hJ<=2}wfrq|CbS?|do#rM?Q1QYtJOdJY$i=X(kpKU8H-7J`UTub`-?~h!V zFKb!8C3+vt>Op)Z|1DkVn%YKf_Z&HN5VpWpgc!m00EroYB_L zcS^E}s3ElC3VsMP_8jJRqTShD4_;%~zV9jFm4%?TLAvBe{E_m=z3n#__s=C^zuDZg zGM~Pd&T;7xwn*s9=cY4@=qLOf$>Y%<5c7UYn0;h7sBBSVaLF7Va+i2Yy}=Kuv)RwO z#zi|sEmQ7$$H|u8!V%IUt%QNUq;NH!q2_4n+hAv`?X8q2ek*4A(9!wsIAG>Y@-9Kw(&EGz(P1yw zf!P=Rmm;i7O>JwR&r`yzSQmD!mxZ@{tvXxByLt%MRUxSf&W8?3ut_cXfkS+eukDTl z=9tJ~WTp>vHuotp#36N@?H(O<{9jAJq=)s9 zF+)5;oykzGODW(X^Xl%eC5?$`#?~Y(pU&Ic4jb@5j~SoA+NcBh`mQC}1TUkoe3;O^ zJYHHsm-b_t%nf%7@IT?O-j&WO{Luqp{jf>poA(WgdZN@)*6gOXvs~NcdS= zG58<_4;_+g|SSWXzVGB@=(>AC1 z{2O{^aD=->($7kxwo8aE!mnjdyoE_?dcWhO zZ2Vf-HOO0;wCc3t>G~c|Ny0Fsd{$Xo)L6CflGO958hDF@zEqgC$sd$g+m~=DUu8}> zpLd)Z!&*#F;7ImJ--0dT!yGHdLHdI0dChe^UQ4R%LN)3uc;Ujk%{qu!KE!&Z)Tq_* z{~kxP({|USLKo_8Am$)C+tP%#dmBR@B&^Zx%QG7m^FoR9>ut}Ahij;QA^k4cl{nUZP8KZ52PAX${?B&Jd zwS-yJ*)ej_b&j?d5@w;112`w_Xq2b>_K57u>GJ)idL zoSJ>UQ&MA#SZ%_<97QI*hNY{-zqn`ZimPIDPQ~_lN|-(C6{6M97B#91+!)mTX`9PW zu`qjTIL>M7-k)CeB($w3(28sPk!T7Qf{KehayG{k1*T`a%q{wT8xNyOpowKA%f&VSP)kp5B(W z)faq_@fs59D@XhlHrb6Ft75Ekx;u`=R{J7*1Ok+UO4VO$*EK)3MQ6K*@$*{JGo^cA zc;{G`wP8cD&+ILj-st2ya9X}4=JHdufc}>WE}n6%Hlguq>oRIR=$P2>LGr5bLB=~3 zgOnbz%0HVPPf5Zkn6%tE6HeFe#b@~?%>GFa*CYsKXL#VQlH)NHYj`{*k;O)eIiZbe zcb5q^QhKZPSBkNpby-E*B_BHS|6F1*20O2^#mV{%7CDm)GxNl^Vi-%l`)kQ{A}fY) z_!R6C8ig$qI%1Hu7-OJA*79E8wZxomz^oo8aeXlRyS7z}2?TLSu+cXBEG@o2pG%5NGwQbE)_l9_ZC>1QbQP;t%V*{F;9_2gT2z*NkEevs{lX-Y zbE$v_q>x}eAr%sZR;Lt@mfsRiFTMZ zpQj|USfQ+%){1quNReLgmDCG~H5n%E3R}fl){rlGxqOc^w?2Q@6VNtez3H``ALYVx zVZE=8%jc3lYE9Jmw3;RF0^q(`*Nd->={o-Z%M_YXZ892rL|gg?2T1hEHtSX#K9Lrd z<0U=5?{a^~DKR{wAG;0794B9xJytx!vZJ8KI;T4&ky(McP!2h9p(z1CRPV(yN~rD2PY z_)30owBm^MNM7T(&m~|9*~4tYgl^>FftovP$czASXAJ9EPQ?*FdwubEEvXPYMTmEm zb&#`sh?V4M4&(c-B~wNyt$!>(vpZI2#?~aq# zJdP)4be!JSbi#aCpK67A#SDLi!>?t%+9eu*INKcuym}<2)`Q&}!@aOxS{E3W&?Sd; zUBil5KIHlQRISFS76a=e%B1fwcbNDpP50triEI66Ulg-O#Cg1yFgvZAY&A&RK|3C6 zM-GL)-YS2@qwQ-v^CPaL;7n|+(^z!!1X`W!cNBF^I}F&QyshkC*(?#@gkiCtXUZqd!~s=nlB7%Dpc zP8iihLZ^aHb;Akh8Drj%SPD4!Ara4H$ULS4pUaG&60@8mCnm}*H>b+rJL}wU>@%cI zz^^4xT!VekCm9keG5ntrOeUe#FxKQWN^Tamddqx4EqAdq3_&DTi%-x?4SAV3m!Htj ze6HU^Vs#mM658%6vN33+2mMZ1J|j)?FLv#fU@t(i`Ag^ezm{l5En82hg@0%~EW6j! zM~NFw0BV$Qh(iMRJSCXO5hZpBy}^crU!?fphvYcxt$4(SS#x?yuH8pACs26jSWl>3 zc^BgexME!EFaqyq*th79{=8%rV}s?w=)T&j{*4y74(kkMKkJO?(XCv|7i+nEdQBLG z$YoNJSQ?5}4<>@|q$E9sc~STvORXRdIpMA~L{I#NZDGVGPsQ`=(bp36F(MPzA5@Ad_I>Xj10!Y+S`jcK^F;K_^^HpKV4_CL%E{= zQ(`hM6FzK&d9W@G!)6J+hmgivPpTTjb^V_bW+Qryt;WJ$lY`u?WYR-Ot0plCsd$QR z&)|7V^u)Z_d=gkU`wynK+iBKUHB*eor`PUr^a9m{degSVrMsdkU;E>0Kox&UPvX~n zR83-V8ZzSizY{jtt#!qztSxXuo~(`aAyF>c0iH1#^O0uM$uJizeEy$Hzyt%(HvBcY zCZ|?!RV>U|Q!iItJf+@Bek%u9Nb9*K#-xCH`8*|x#LXsO$!{*UvOs;8=AztZ4L_m7 z(ul@tTacCy6<+r#!DJvYm7~aFUqN+vwSk9Z6#GPHd%|67aL%H8e0t3oE_8DC6f3E< zdZnDr{b8xd{jk0u(p?rz2`4!^rfmzuKA-<;OBlpgh}wE(iyG@!pE9kTizTo9F$Owh z#ENpkv!8VdYg>G^m(M%SC`+EeExBB0b3H5-OFhC5xn#^5`&pML+pMET&FAx!fSG1L z`2$Ct>xxT!dR2a5IqkCH=a_X8Bwl*G4cCav&r6t9WWr9zy6hx&6EdR{GEdBrh?ST4 zjQqSJd0d{7gfWrnQE$oZslOI8(idY{U%R&CS#`uW`#rwL*?Jnc)Y~f8+9lN1F>M2P zi?nKrZ}xjXB^J9|rsY02bTa70P36vZO3rXx%`tv=`JOw?M__X92h_P?cz29*bCVg6c z8hthPK#lYz*b%($w>NiL$NMSKjJ2&NWZJ>>KFrA+SVu$Rv;2r_39OdD!4Er^9#2V| zRnC(=x8`%Lt@M$e7|Xi!A~wFzo5sM;vX1e1N|@cKUZ2Kjwe9e=zO0z#>+QzMt@~V_ zlCaD}v(xs1oV8Y5T#H=2) z5B4NOxeMSdgv4rb&H>ivDPbmOc_+-Aco$nb$5BK3TDIhA3~J3Wk9Qnq#CnS@Gxwxh z9N~jxKH{_dj2Y5h{!Tby7!s??*mQzjE4D$SJpm7i6(&2tDodSLMtXfNNf?F1+V*EC zE&XtdcQ)#Zzcd+R107d$TwY6<5$nAZ(m4aeygQV8g$`*oBQYJ=jZd%ji7c}@g2ufYVS617d4F zNxaUdSDt|AwFznL-nQxg=o}6lZO#us5^HRmg;t(S)~rDJ-*aBC^2bcx}%m zGR0)__E;Y44fFD~J`sbIjHqAp=PK!U!9*N|EFZ%DuO*tDw!suq;w=oY_mtcdbV&4} zZTKN2jxYFFB4*XGe8}~AO3?jQvNoZu*0`k(`eO;*s%yn8A98Ck!8}jNG^sFsKjb&r zLF}TIy$vI;h!4fc6!3pa^aQkB=RZf+~Vyq^*siWS)mQ^e$?fo6|9|wU6D*J6`Vzm6X`B13#RRA8weVBz z=e6WIxoH{?{8rno7z;~-(YLrJZSoV2!^nY@80(B>-RHHWhZ(bM>08%pt!KrnJ%nop z$4z{^aHIS^{PhlaTOlsA=j6O$niywmZP; zOMIh#X|$Awm?kr=5ww@>*LE#o?nlzC(@^UVcr7IdzFNMHRdZav6V?nz?dn8Ei`@Zl z^=tXiN-YM=h&;g^C!~B^6KciNYWK57`eH2W(yg|c!DtNl6PywrgJT^#Vo|8YEc_Z@ zZ4LjYBx4uW%;>l|!VerFcj>k=H~*(ZPpmb`R$SM+mtE_t7VGJ6ZT^yXw)Z$aQ}cpZ zcIn!2*r)Spooel~osw*_`wVvbs?}Y}m(x7iW*wR0v;2tHr2jk4TIRK>93_UGy$46V z2HXBm$>@$^@}qf&!w0(~iy!YevMe8Bx%7U=8DpFC7ta)!v%bPyFZ0tfh(S`Q zs7~0BhnE`OPf5b6BtO|5!%yN52n=On%pJlwVe`cipw;t=>2`Ae%KEkGU+lT)?_e_^&Dw=yq}VB z#?};^_`=d>jkm;fT-!^f9?jz^xsCDG6YQDt7^cWCEOkm4@msOPM|GBpxwhnZKP3y3 zoTV#Iu)7t0YdF?&tXO>=kEg_*r*E&`IqaJD_p+?!g}h1?@BTebon7BFMr-6%Z})Hy ztV?BWmwfPRe9K0jNbBYQcfxuC7E4dQ8lM;oHY15gjDnQ>fazSe{hyN2e!2^d0x8Ei{tm|&sq z>&tq&ENGwqlOv9#|J- z%SJVggCDXMw_?%f@>8|gJ?NiZhM8b5?-s5V*W-MBcN{$d4>opH%UgR#y=EPk*Aiw` ze>P`iS?``!xx{7>=>`30iA5MAKHw0435G8Q%kPv4lBlomT9QpPnaAS{t1rtZ9g5$I zMPC@>5%W|U{60?!x))NcE0X>!@yC0NF@zMyK_+1}%fPavIO zZB~26%)UdqB4bz=6x*yLrhQylV)gj*l<>Tcp~oEyb3f6pw8t3MW6HHwv`I1XN! zkta2NwHPz@@Omixj2nLD{q_>qFOHALJf4yoV?6- z`?aLaaN+7j)0{?Z!D>8sTYk%@@y}}sCh}-$?G}8HTlFaQxF+lN8bABs<$wRRmoI<* z!_R!}b02={i+}l7e)hrl|Mr_Ny#MttKl!&m`Hhc%@!j`7`03MpPUFmn;e*Q=fA{O} zzIn=n%;Dod`}d#x%RhVb!BZY%4)6cfUwrc8|8V*Fc*8ty#E(JfAdtIb0{2w4?h0YFFyJG%ai3(9$^mu?}O_`{OAiW F{~zlj`%3@- literal 188270 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 Date: Tue, 21 Jul 2026 01:25:40 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EA=B8=88=EC=95=A1=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=ED=95=84=EB=93=9C=EC=97=90=EC=84=9C=20"=EC=9B=90"?= =?UTF-8?q?=20=ED=85=8D=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NewExpense/ExpenseInputViewController.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/SpendLearning/SpendLearning/Source/Presentation/View/NewExpense/ExpenseInputViewController.swift b/SpendLearning/SpendLearning/Source/Presentation/View/NewExpense/ExpenseInputViewController.swift index 9da033b..08ab2fe 100644 --- a/SpendLearning/SpendLearning/Source/Presentation/View/NewExpense/ExpenseInputViewController.swift +++ b/SpendLearning/SpendLearning/Source/Presentation/View/NewExpense/ExpenseInputViewController.swift @@ -26,6 +26,7 @@ final class ExpenseInputViewController: UIViewController { private let amountFieldLabel = UILabel() private let amountTextField = UITextField() + private let amountUnitLabel = UILabel() private let amountSeparator = UIView() private let memoFieldLabel = UILabel() @@ -92,7 +93,7 @@ private extension ExpenseInputViewController { let digitsOnly = (amountTextField.text ?? "").filter { $0.isNumber } let amount = Int(digitsOnly) ?? 0 viewModel.didInputAmount(amount) - amountTextField.text = amount == 0 ? "" : "\(amount.formatted())원" + amountTextField.text = amount == 0 ? "" : amount.formatted() } } @@ -167,16 +168,20 @@ private extension ExpenseInputViewController { amountFieldLabel.font = .systemFont(ofSize: 13, weight: .regular) amountFieldLabel.textColor = .DesignSystem.subtitle - amountTextField.text = viewModel.initialAmount == 0 ? "" : "\(viewModel.initialAmount.formatted())원" + amountTextField.text = viewModel.initialAmount == 0 ? "" : viewModel.initialAmount.formatted() amountTextField.placeholder = "0" amountTextField.font = .systemFont(ofSize: 20, weight: .semibold) amountTextField.textColor = .DesignSystem.primary amountTextField.keyboardType = .numberPad amountTextField.addTarget(self, action: #selector(amountDidChange), for: .editingChanged) + amountUnitLabel.text = "원" + amountUnitLabel.font = .systemFont(ofSize: 20, weight: .semibold) + amountUnitLabel.textColor = .DesignSystem.primary + amountSeparator.backgroundColor = .DesignSystem.separator - [amountFieldLabel, amountTextField, amountSeparator].forEach { + [amountFieldLabel, amountTextField, amountUnitLabel, amountSeparator].forEach { $0.translatesAutoresizingMaskIntoConstraints = false cardView.addSubview($0) } @@ -236,7 +241,10 @@ private extension ExpenseInputViewController { amountTextField.topAnchor.constraint(equalTo: amountFieldLabel.bottomAnchor, constant: 4), amountTextField.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16), - amountTextField.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16), + amountTextField.trailingAnchor.constraint(equalTo: amountUnitLabel.leadingAnchor, constant: -4), + + amountUnitLabel.firstBaselineAnchor.constraint(equalTo: amountTextField.firstBaselineAnchor), + amountUnitLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16), amountSeparator.topAnchor.constraint(equalTo: amountTextField.bottomAnchor, constant: 16), amountSeparator.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16), From 4d1b7eeaf665bf5e493e583c728208314009652c Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 01:40:52 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC?= =?UTF-8?q?=20=EC=98=88=EC=B8=A1=20=EB=AA=A8=EB=8D=B8=EC=9D=B4=20=EC=95=B1?= =?UTF-8?q?=20=EC=9E=AC=EC=8B=A4=ED=96=89=EB=A7=88=EB=8B=A4=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=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 --- .../Source/Data/Predictors/CategoryPredictor.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/SpendLearning/SpendLearning/Source/Data/Predictors/CategoryPredictor.swift b/SpendLearning/SpendLearning/Source/Data/Predictors/CategoryPredictor.swift index f4a5ec7..306ed82 100644 --- a/SpendLearning/SpendLearning/Source/Data/Predictors/CategoryPredictor.swift +++ b/SpendLearning/SpendLearning/Source/Data/Predictors/CategoryPredictor.swift @@ -113,14 +113,18 @@ private extension CategoryPredictor { let writableURL = supportDir.appendingPathComponent("CategoryClassifier.mlmodelc") self.modelURL = writableURL - guard let bundledCompiledURL = Bundle.main.url(forResource: "CategoryClassifier", withExtension: "mlmodelc") else { + // 이미 온디바이스로 학습된 사본이 있으면 그대로 사용해 학습 결과를 보존한다. + // 최초 설치 등으로 사본이 없을 때만 번들 모델을 복사한다. + if fileManager.fileExists(atPath: writableURL.path) { self.model = try? MLModel(contentsOf: writableURL) return } - try? fileManager.removeItem(at: writableURL) - try? fileManager.copyItem(at: bundledCompiledURL, to: writableURL) + guard let bundledCompiledURL = Bundle.main.url(forResource: "CategoryClassifier", withExtension: "mlmodelc") else { + return + } + try? fileManager.copyItem(at: bundledCompiledURL, to: writableURL) self.model = try? MLModel(contentsOf: writableURL) }