From ba08e352d494d1da97206855b0fc65f2accb74b0 Mon Sep 17 00:00:00 2001 From: KINDNICK <68893236+KINDNICK@users.noreply.github.com> Date: Thu, 18 Sep 2025 21:11:46 +0900 Subject: [PATCH] =?UTF-8?q?ADD=20:=20=ED=8C=8C=ED=8A=B8=EB=84=88=EC=82=AC?= =?UTF-8?q?=20=EC=9B=B9=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 121 ++++++++++++++ Partners/메가메타.jpg | Bin 0 -> 18603 bytes components/header.html | 16 +- css/main.css | 356 +++++++++++++++++++++++++++++++++++++++++ css/portfolio.css | 309 +++++++++++++++++++++++++++++++++++ index.html | 33 +++- js/main.js | 176 ++++++++++++++++++++ js/portfolio.js | 47 ++++++ package.json | 30 ++++ portfolio.html | 155 ++++++++++++++++-- server.py | 155 ++++++++++++++++++ 11 files changed, 1373 insertions(+), 25 deletions(-) create mode 100644 CLAUDE.md create mode 100644 Partners/메가메타.jpg create mode 100644 package.json create mode 100644 server.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..50ef665 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,121 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the official website for 밍글 스튜디오 (Mingle Studio), a motion capture studio located in Incheon, South Korea. The website is a multi-page static site built with vanilla HTML, CSS, and JavaScript, showcasing motion capture services, studio galleries, and client portfolios. + +## Architecture & Structure + +### Page Architecture +The site follows a modular component-based structure: + +- **Main Pages**: `index.html`, `about.html`, `services.html`, `portfolio.html`, `gallery.html`, `contact.html`, `qna.html`, `404.html` +- **Shared Components**: `components/header.html`, `components/footer.html` +- **Page-specific Assets**: Each page has dedicated CSS (`css/[page].css`) and JavaScript (`js/[page].js`) files +- **Common Assets**: `css/common.css` and `js/common.js` provide shared functionality + +### Component Loading System +The site uses a dynamic component loading system implemented in `js/common.js`: +- Header and footer are loaded via `fetch()` API from `components/` directory +- Fallback system: Static backup footer is hidden when dynamic loading succeeds +- Components are initialized after DOM load with proper error handling + +### CSS Architecture +- **CSS Variables**: Defined in `:root` of `common.css` with motion capture/VTuber orange theme +- **Modular Approach**: Common styles in `common.css`, page-specific styles in separate files +- **Responsive Design**: Mobile-first approach with breakpoints at 768px and 1200px +- **Color Scheme**: Primary orange (`#ff8800`) with gradient variations for modern tech aesthetic + +## Development Commands + +### Quick Start (권장) +**Windows:** +```cmd +# 더블클릭으로 실행 +start-server.bat +``` + +**macOS/Linux:** +```bash +# 터미널에서 실행 +./start-server.sh +``` + +### Manual Server Start +**Node.js (권장):** +```bash +npm run dev # http://localhost:8000 +npm start # 같은 기능 +npm run serve # 같은 기능 +npm test # 도움말 + 서버 시작 +``` + +**Python:** +```bash +python server.py # 커스텀 서버 (추천) +python -m http.server 8000 # 기본 서버 +``` + +### Development Features +- **자동 브라우저 열기**: 서버 시작 시 자동으로 브라우저에서 사이트 열림 +- **Clean URLs**: `.html` 확장자 없이 접근 가능 (`/about`, `/services` 등) +- **개발 최적화**: 캐시 비활성화, CORS 헤더 추가 +- **오류 처리**: 포트 충돌 및 기본적인 오류 상황 처리 + +### Available URLs +- Homepage: http://localhost:8000/ +- About: http://localhost:8000/about +- Services: http://localhost:8000/services +- Portfolio: http://localhost:8000/portfolio +- Gallery: http://localhost:8000/gallery +- Contact: http://localhost:8000/contact +- Q&A: http://localhost:8000/qna + +## Key Technical Details + +### SEO Optimization +- Structured data (JSON-LD) with LocalBusiness schema for search engines +- Comprehensive meta tags including Open Graph, Twitter Cards, and Korean platform-specific tags +- Sitemap.xml without lastmod dates to prevent unwanted date display in search results +- Favicon system: Both ICO (`mingle-logo.ico`) and WebP (`mingle-logo.webp`) formats + +### Image Management +- Studio images stored in `Studio_Image/` directory using WebP format for optimization +- Gallery system dynamically references these images with descriptive alt text +- Logo assets: `mingle-logo.webp` (primary) and `mingle-logo.ico` (favicon) + +### Form Handling & Interactivity +- Contact form with real-time validation in `js/contact.js` +- Lazy loading implementation for performance optimization +- Scroll animations and intersection observers for modern UX + +## Content Management + +### Adding Gallery Images +1. Add new WebP images to `Studio_Image/` directory +2. Update `gallery.html` with new gallery items following the existing pattern: + ```html + + ``` + +### Adding Portfolio Videos +- YouTube videos: Update `portfolio.html` with iframe embed using `data-src` for lazy loading +- SOOP/streaming VODs: Direct iframe source for broadcast examples + +### Navigation Updates +When adding new pages, update `components/header.html` navigation links and ensure corresponding CSS/JS files follow the naming convention. + +## Business Context + +This website represents a motion capture studio business with: +- **Primary Service**: OptiTrack-based motion capture studio rental (22만원/hour) +- **Target Market**: VTubers, game developers, content creators +- **Location**: Incheon Techno Valley, unique positioning as Incheon's only motion capture facility +- **Technical Specs**: 28 OptiTrack cameras, 8×7m capture space + +Understanding this context helps with content updates, SEO optimization, and feature development that aligns with the studio's business objectives. \ No newline at end of file diff --git a/Partners/메가메타.jpg b/Partners/메가메타.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f49d5d3eb07ca0eb12a07416087b0ee46195364b GIT binary patch literal 18603 zcmeHucR};Fa&xZSw`&M$BMJzTD zf^2Oe7z9D$kgNa%34xS=kQ@jE=F(6KNDK1Qyt0P^qCen*0KW7`8dMaJ`;i7%0`iau zj}Gw91lBC1p9`q{R2KmO3XlTuF9me?>E$5(U0^FeEeWbiUCQIr0B`}h4{ISY5K9BU zDG3V+16)8~a5G@}cfdREPlW`4s*C4i(*T?wTG1g0hsQ+);ldSl5L!BlYVO#u09;Je zCP+tHN8dga&_#(0(Wy3WI_m93Z{+!(5{v z{Ko+aNSuRsV*&|l>*ylR3C)k8;Ei*_3+a35Y5cU%7HtShPyaJr@MC^)xe!z!I6s~O z=L&@8(~#w#V=maullWhTOxs9D|8HUc3=~gV3roCJ z32|u&3F#$Ll2S|LWTmBLGB(r$YqQ%P>En2jk z7c5#nS7ph6g#h41e2ubUL!X-k&>z5)#OdWlfDMcL75nY~E zSZSuLBIoqLPggJUXqUOzhIHpIZ@>CS>94qa>)6H}87=`94~y^yE4y8X(Tu9&k9wlt zF?XtFUO7>n_4vie-x$Dt>PxkEWYD%lSv?ecd3+V?b=9{=;s- zMA6Cs3s(a?dU%0_f1d-@KUv@oK+;0|HkUw1X!7;Pk3OWNurTz|(s+54f9K(keW_7H z-w&#n<0Y^QTxhzxWsD2CXjftDwOSR-<5O66t*f%D-SbWz zuw|V`VG~4VzIR0*gqKW>B%B=#|41ytTuV>K6H6~}ArIY)r_M}AtUgiuy7j}hdYxR2 zTX}h~R&vFQ2T|{DH8fCdCfzt?DO@PB-{p=^V%yW`(*1CMPmJ}Gwqiv$m~?r=nOs!Y zSD}c;Gcg}uD|mOmnwBSYjuWlir?0JaYZ;I2e0=Y<*QUpf?KR~aN!8QmhmJ)?#ME4H zy_Z^}>(7OBS*IfyChF%isUJ-2*A!PK-X9OU{qbqjb+Ns{_uq_ld1_8;et+fvx&>P? zS@+6y*YN1HyUmB&dGT$RzUXSTK90I&bXeGcEOKV@F3O(#ViqM*Gg0?G{*^m{q^kVF zGeM^`d1w&yVFcxp$URc^s9j{X^r`Pt)%Fgae&wsn2ebzsWon%nzbB%q>`#^x- z;5kHMdDkB=^DM7Tr{d_dvVS~&W5k6@S8*Zt;Ii{LqEqdRT2XKGah*C(Oz{g{!eG|L z^Z{}~{KS))z27D#zi#40z08~}(byv$R@5DTBG&tsQIc&-z-;-riA_*r%B&)V(4UZ_ zUplpavhn;YLfN*92nJ%j2q{)Hi>`-WaEcDBYNaTDdiXHp8JbW-#SnKtJ4pRzNImu5 zm9sS0bNb1KSe8$5Y37u*mc4#s-~{=+X6@`Z6Y@&DIr9~EHO^GA6r0nj;)(ZWGJcVy=0hlSVcl9y#W79QUsMy+W z+J^LWx}h(c+*;N-BZv3js&IhdeQ`Yg0&)Mer{#;;HQ{ycxc8JVJ6Nk4gKvxk47F;G zoq4$N-uX8p2IC&)AKN;v6;1D6v$peAPYP!C+Kin$v(7K!!X~P9AgAYpLghB)af7({Ni1Y#fuFsYWRW|y6yH;)Z zF_DUSnd(89Kz;P7==zp$`qR3cK`vxDRys}@rIZr`IlVK;>l5DJEC&fs?-Fh5KBCA8 zNIC^`aMs@W#jMg7e7f^wr|InJnJuKs0}1K7KjsY;|ADHWrj#aBa7GfyQ+ZsdbEYqu z<*Jf7>yrLojJOc@759^@@t?Ytu9h~m+05Ejn5UOPkmg#T+kyYJf)0?xf>O0s&=3;m zo#EO#ItFV6jUiElp&rQ2)!e|Zxfm|@RU#;>(L<(riy3@ommHs}5x?vm2p0p-J9{8V zDm)S!hPRK#hezW#k#@eJXcr3)7XX@TY_5VsBn}tojK)WY+xzYJ=ixttdhz!kR6T|ipT<4_ z;RBUtKz9aCGJF9SuOD#)jarVSIw1T!N2nZtH0?CyT8-5`vl___M!D zf9kJ&I39}&ivr~*)8lM&=(X=V<%5#q7r6riXWfQ$Cqj{BXL zh5Rd?S!95VYZ!i~vQr3eWEY%SNPyzs!JVQ)@O-#KY{>So1$4;+Xn#B|a)&S8m)8S6 zlMWb%sQEd;OY@LH%wrqcjSie6^Ot5_fbH;i%{u#s%)uQZ{kMDaDDd54|43Z8dk`87 zE|{O#FwDI87V!$408eH(JRTPsf(yg^s3JXI!eiklxXe7<35yB(xj<^Z0Pwbe&s!-% zpXPXgzJM5<%JVUQ6>a2~E&9(Y&x%no;hw;|#IzHy=U-LpteqMqff0zV*y7jhRMI()bn2W?A@OU?2S361Q~IIl>I?G;0RJ){A+tZ{EuRgig=xuWO*AS5>yM6d3)#(E zSOUMaBf>8O;HAKe@iW#terih$6N!rs{{##ymge7i|y|D?Mr+f{0p)~5H2#_Gz5#8mz3;W=dI?!KmcL^=s@3S{&p&H z03C_{8+5mM@UQ4nei(CH2rlv`;Vzk@Z)Ukrz>~2ZE{rE@2|O+w9LiDXpZ6=t5U{cR z3sl;VzY+cmTnY?v&|lH{Yf+8Y5CrakAVK68etK@*E#arZl@%Wg+!q1Io6rLMJP#id z<~Kc{H~ayu0#@ojR_s4k>_1lQKUVBNR_y;NE0%vgtOq9o1i1qb zP7t1&W(oxWb7-*7lWOB)UuBV)sT{e#hXML#sqflQPp>M2T!*Z>nH zcYRxJ+i-Jq5Y{R_673vs=i(n9?7t;I3As(tIMygOG&~fI_f?Dy4GD`fiZxN<6E^~B z9vH5q$Vb5kn<(*bX%#(e9Td%Rk!VE&Ep1JIZEYPz!!23}13f(hZ4E_WtZT!y_2CE| zO@x7www@8fQ1ORT0`1R6Q**!iO^kTX&G(W#i27_AsxEnFl9j@Ytg3tU?VuA`#~ zFf^m$!tlPanqg7O^AvWVqx>VWe2Z3*M-fG@b9RnBMhbrDzk2xe; zTmUvO?r)KJU$fM;w-{+_|3C#}9{>#D{}egE-zX3l8R`qViw*U~pyB(&(HK4rTU#UR zuqeE5m_OQjhlvv4QVWX>FxrCFHPkT-)YbGe(DT#WvRMzIxkU#ZsEI%XqR|1qy1v?a z0rT~D;QXU`W5KI$w!?fU!eWc=W_>d={hd2>&3EYQ?Lg@7G&k4NF*CF$3TBhjW9Fnse{hr|55{>!J;dEWek ze8VtkAa=MC{4f3gYnS;_`e`GWM+dtLFS)R-ZJXbI{)zvQz&{fBM*{yy;2#P6BZ2=z z68O_FLWcp>A_kZ}KRr3w&NJfpPfi3EoDfI}34;(kkr3uN5m6Do6BQK`6BiQ`m5`8> z1W!|bbpk>{LL$N<5~89KGLjOKGP1lUC$iw-3UEIn>ABpm^%in-Pfjca&It*CHyVFF zIne=6PA&sB#eRNq0tt%<35p6pKc1WbUVknH7gIkyIS~Y`fy+8kF(D8LMhOTCEfI!9 zz?&0ON6}@z;LXW$owPzRxw$7FZ*^DreNl-#`gMc3o^$%;TmDfG^>?Ud6jd3lbP2#e z>K+!~cuYca)#~G!7CWz0yY;xL$ur(DdCyMJ(I>Lzo}T3BoxZOXyd(-F%@wH z9E(OANz*B;{IdK(*IQ*XIVZo!uevIF=0{hgJ8!t`f9s)GRJZ={j$WJq{`FPM3@f=_?f~qg>q~N_ zyI*^v7HMj%imU}Qf?sDtnuPV67 zGp-rKyjp)D^54vIpa0q^olw`a3E! z^+=6uCl_KpH9<0$HP;>r=xu1GJ!>>bTz7`*)lIvXxUCUgXF9wSHuH!RQOt=#G0ZuU zN1uDesd{AWWHwf>yLXipQ+odQ7v1`=M(c(<2`HdDvhGyVP7(ydxd@`+-q=sx}`}w^irnz zB`%pFbtsDlZzHm%F?7*FRx0asR0#{tiEw8OI;Hot5Zr4IBziZyC4Km+Jvi>P(|qsi zurATXJ6w9R+&`~8$A#eRES4`VZ)-CpXULshMCAhARB{d86ZeduDTxN+Age7&wYcH1p` z@zCn`A)HP6`osxzodDJ)v%df6Gm{=pf(6T+WjLCL>!hCN1f>(Ti5;eKYdo?IyYy?M z^8mV9wJ053mGe07)%h-0mlc8QU6yWdLS+n@a-mBnfj-HHE6@5B1*gj!$aavu$-~*uD)3?zuW<-bF_sB~McCvKID8}!{EQoOkO_U-DLWSccP3UZ~T zo@bDP1{W(hc7f8ZB<4VDTL1 zmF4?YUFTecA3GN?co+CFCXsG`;VJu_0{iVq%=B#M1uX@u{zKm{BUXBmCCi%fQ?&~c zyAh~T)KW3|lP?uVTUB3Dna8QjBODh{-M`zHiOJnoGD_+wbL#1)Fom%#ofbzOXIg(# z+PGo_fa&BQDp$KD1O>qNx^TB_Q6KV@=ojJmRgc)m;G zHnR;e##f>pO{sEmI9d`L{J*Q9Tb`#eQfb7|C#Oaybj|mFFT3nr-L=PfNv_q`4UZ$@ zFR6bOE^)k5(1bEO<{-OkxI&FR!X9VeWhXMW71Oy;TYJ2TtXWTIaj4{x4Yb)FLBFcX zI7~wRm}#!rACIlw`ZP?6dB}A}AM4A)v7}f*Up+jrL)+Pv?7Hp^x6`G9E@@#eSQ_L4i$z z3q2!G%xrA(LNdQ}vL1esK$50KMCsq%so!jq?^Q4;7bf9GaZ%>Xn5?N)Q>j{U*)1|Q zX1%`B%6F>on5yU$gqK;m3A+d-H&B>3P9&0H$xs?Mzr}vTe$>NGi5m4N-}wCUJ)1rI!!f8#>1+*EE@4UX z?Oe$}{3=LXNS{K7A?XT4<(hg0O-?W&Xl&c={+c!LZi9H+=lPxk{TI&cavIvSD-pdu z&N)j)-8{LmsCAl?GFrCGnhDQ|R!_i~_Lr@vbkzh~E;Wq`4L@piHRVZ@r*as5n%_7&^w8RfZ-IQ_<*j%&@Vz0X8km&3$tg%xNVly((hp;&iVF|01yE8{mT z|4}RUAno|w^ycW+AT6cZ^jPc%4SQopiOtK*oC@5FwUfSFwUZ zv)A#KYLxXlvRC=`@$|x8v`fU@w7bWW8^Cz`b0XCl&J3*t$w6imQ_7a1!3nfErMGHI z0AZ*e@r2tWsqRfu%bSL{q)u$SL&n#lJ&^6pUH(il&T1;Sw$X$_6v;pyd=j0wUOI1cSaz9Tw+d+fUOWPc4^6Jr@}6x^}y;avuBCd z3Mz(`n^60~De{0gb%sJ$qzO9Gw$Ra$uxhtoBAC;xKLNf|>gH zb%oT&8|&^IwK*YuHf3`0&fgDPh&Q2@6Al16^?(Qwc8CjIk2}bTNdNA|TuHN}@8TTm zRX*)_aA!}b)O6ybHXTvXV6_vwqn_XKdVIf1)-hlfJXOB56Zp*xS@v*6mdRED*$Wh% zslnKu4$^sDv!$L#yH4b#%g_3B>ug)~fV`x|tldt1WfO^c7@3c{gaU^#GmFYhTW80d z7~NJi+6iO%b$Vp9PA@7{0Z%BO$0dEJTy^wv+QTLHT_&4cDD=;!?!42%Ed9A`j}*P0 zR3gV=rjoDaE}ER$>{s#XHPF1@rm}znRGCUwW&3s$KbK*WDaMB?xX|XU2U%4h0$Ic)N=9VN-cCHVS!V_qeIZhwJaw(%(kR-1%@^2R1d)(#e|6 zP62DS6}fqg!c1T1{-WBxGqqLE=n%)VrYhKc)aKb>H_S07G0!+e9e=A^A2nprZgmEB zpSS@@|BYfuq$7>FkS{opHUN*^(2>Ei5YQob6I>q#)+A21pG4cloh4RVGd@2xrI0#TeAjM#48N+fsQi_bv|6Aa z*muaXm`TF#Xh^ja!<$pTlwUEpHkjZocTjBPS_T)|_5jHG1amJJN~dh1XdEgWMIo@c z{^KiaZE556+p{Cu*UG4tL@f^rVk#EsgZyXZ`fpu63^n=0uyR;3tSzkOsG>I< zEN9Pq!ok}Nc9g|sTk@d9K(ChM>TIQWF+>@`gWQ(N{vB-n!-kwk)C}b3b(DTN0`UVE zx=3QamyvSst=4n~tGqRPIzK5Ps(8PH?56!%ONaa15?!TlF21iLzc?tuy%DT2Ee;mU z|HeZPSQY+7Ag|Bzw&^13Ozqvs_C|TI4$Rob+)V1p9@|!Btd{x5;=}L0#Rh8BZDWpBGm#oL{kfx)XP}xLgGKrbg z52k$Pfcq$hJ<`vnWjcSjJ3((uX`;}kj=-1a?maX9M8>Dr?#uZsM-A6|Z8mKsQXU%f;z7N%n6koh@Ly1f!`|7*Au@iBXR7Sk|~-*7?CdiJ9`EUur527{hD1&S+@gZ z&M;2(hT(DA18sfgUIs=r{k?^EZ<~8)`ALd2wtS}e**TJqxZmMxu$^TM7e^+H;?y~j zo!fg4QCR&fPpN_CP@JDXsm*`mR7%T`y3T*gxk3Oz>^F_Np&*Gc zE*d3w8a&R3bL!tox!AtAb#=C3Pf^{0dbbo)Eosvu4zPH?il;%8tw?%DpjsPYH{+&R zLBTeM7e@pyx~$nhT;a;jCvg%YSWJ2l1&#!ktVTm|1*<%GP2R4p62?_AK_vI;cONRV zhlRIWB22yVUy{dh(@X7Hk4dvhonXzsHZXRbRLEpsrBRevG1iQnOub-~77Jr~*z5hk zhIH{yp9+P~r4_t~*}?9EUw;r4xzI+iQ;c?%BlU?|)hLs8T5q1-7X2U-|NV6q(hp6V zwoC`3&NZR7ast7ml>=VwJt>=d-s?#gONzEe(|H%8qav=@=PBAUF;;_?zGX4vRaWNW zLlN$e6qw^Y^V|LzwFAd`+Ky{S=904IW%C=FLPy8z#r0F%sC{{%MYnFH`N=qbNlZ&I zwu8-@FzVRDV(fRS>Bdis;_6#a>G|$peO+j14A+|6yT7C7__vEJQ9_$4a}g?An|$1gv( z>C5KW&R1{T2Y7ci+!u3)t_9C9GfpahI)ZFLc2Z#vy*=1j%-!}xz5O;DY9?}ae#;qh$>;ohY{x;qIbZw7^V#OFXiWm9#1ssV}+#L^mi*E+E7(!H|Q*UiQo4>}|! zy*Ro9AMvuJ$%)GPR!5`fQ+Ssl@`{YQQF!K*!Uk5_q5g88+EX`nD&1c3U6numm0gO?&kP2}#Qj0>@0OwW%mEPH!5Y^)2#?;Kem5=J5TD4+p@RN&e zNUxJS9;Dbjj!U`gS9*{+=$OihyvQMVvh-Qj1q$5+pkrN%k0^;7cx!zyy8c$S$*x(C zBVV%8x7whlS|vXvQ*gjCsyIQR)0V0bnI`~tg>q@o`fk%eMVYB!ZNtrE6)r>~=+v>_ z7LN$HNh2QxxX4e|Us~&$PQVg7ej{w>L~sIG&P+__J?mqxOjK6#bfIi9wp2wi!OO(& zwP4-pu={%8EbI+)WCPvj{X35$t1j22`jW{7;5WXI*mi{bFor$-&?A8T2WY+9gJv1f zpOH%zw1_SXNHpP%}c@ zM9G*6J{^UPC<(e}oBSlEF3>$F*Gdp!)>8JQ`ns50{Cd#)yTp#Qko_1%FPAuwjAK>E z5t8DhI_@frTBaW=0T!M{LpLj?Bi^;V>aumj>O{AihEstj8v?I-kR{K)BtJmXm63Fy z;B6q%SKt~-iU}dq{e3a(qKGxDbTy45+}n#e1VD6ixaqY! zqiGYbWsA#!_gfN5 zU1wNj!A9;m)p_=N`=i$+C|h)uq3`w91m9Vqrdd{Cu2N;AyjafBCEa>Zjv&tp>~sU7 zDFA}DYo}`lN?jzf9@{@7KMKB#bzYI8PfjM)rF8)zY5~E1B-SYqHw$q;hnHXfzsS>oqX=%G>+ZtHCDld|log8!RdQ zE?Moq%MJ?&cAfW2r6$yAB=d;QV=&AlBJ^S*cf8$zXqvaQ|p|Y06~T z4n|I{eud6`t%kay_uu=6e5-?w4|}gU7?Vzuwyi!=WKBJMDql2`AR^B%c)AqTYLjKPz%_^+>IA3#J_mZ`hrYMx7A9;3 zwl#CtlA7Oaa{|L<;pD*=*1C&MTU4le-C&*OB+$V4JQ_u6nU@heoC$FkYKWbv4pXhG zLDArx>c*!SuurYNu!`_u_@jtM(aiN7;mM9qV6#^0H-+}&n^30|nAuz?+b0ZcM;dHq z50Y*`RMP7{bh_NZMTwIrFc7wB1f4LPi? z5m^lmntDsmUXo!iSliejnkkws*d z?G=I@YZ}$LW=37MW&DlO@S}EI{DEw#D~OW?4pI%DAB$%RM6zQicDu4?!J1eMe6R-y zv{@JTFp%e|d-{mSN-=L%Sti9uO} z`zVn;n#86*Il{8Q#IUSca2?hRCvs(*j*O~(kKUEp_3KVVM9oyk^{q1tZXvxMDD%!~ zf-hHCT3XCW06z|dHfJ0J!T3mmcRM(uL!xCnW!rV?I5G9Pj!_$qOMlXfCst5OR7;d? zB7g6q`R}^12}Usk$5RP`d}S=2c4i>3=Gwsi>CjGv(y{*MS068pSf6t#Qq-*wNsrKeWbH8(WAwLNSy%AB4>e^e7ue_KmK%Mw@x+Nz0|U2+Qn%~_UrQY=^?D*M~vdS&l4V$z>0=P`-=a;zBn(Zunnei9H*TnT*drG14fuH1?p>b_ zHHYu%92F1QXY)$BKw`@tn1rl$fxdUV8{K|+eH`(Aw^N(#&yBpzLV4W{*urpUuGFim};l(_y5b{Jg6tN^PVO zYSiSNhheGv5H4Y`q6nPK>t)`1ntp4ld8p^Uf^j`|#8Sj_uU(tCp5j!KQ=U|-j}N1f z{4y@sw5OroSNTFfWsjy`)rl*Lf}@*VmsdiP-SvJYqp&B@of}y;Z9M~fSQ#w?$n#-( zUmn?$oGXuh`hDcV#cpg`vdi_2dH0(<)duBoa@#+61?H7{yus$yoTx3iHdJ_)Y2R>4 z^=N>JlhufHsqB$eH)Opt> z*DXybZXeje8j6@?CYdqA5PBwGF^U|utC)tH_9QX z*Irf=dyTh)r?=e+I<+6?p7^eB=x4fVDR$OaY z>?ym))rH_k>>SpU#O6>lhwPd9_y5>gwLIQmJr<$8|D5X3F=N-0ua;LF9NqdJ-|BHK?QmV}1SWmGaLXjZ==MG;M$VqH z=_65Nva^)}8>41a1vzs(UwUfRWnN{x__7z9f?J~IwffVAcbPfTds$06pH*t^aeaTA a?47WgT>B}xuJ-3o @@ -449,7 +449,7 @@ @@ -478,7 +478,24 @@ + + + + +
+
+
+

파트너사

+

다양한 분야의 기업들과 함께 혁신적인 모션캡쳐 콘텐츠를 제작합니다

+
+ + +
+
@@ -494,8 +511,8 @@ @@ -510,7 +527,7 @@ 최첨단 모션캡쳐 시스템으로 여러분의 창작 비전을 실현해보세요

diff --git a/js/main.js b/js/main.js index b5f9db2..a815bea 100644 --- a/js/main.js +++ b/js/main.js @@ -5,6 +5,8 @@ document.addEventListener('DOMContentLoaded', function() { initMainPageAnimations(); initCounterAnimation(); + initVideoTabs(); + initVideoLazyLoading(); }); // ======================================== @@ -113,6 +115,180 @@ window.addEventListener('scroll', () => { }); }); +// ======================================== +// 비디오 탭 시스템 +// ======================================== +function initVideoTabs() { + const tabButtons = document.querySelectorAll('.video-tab-btn'); + const tabContents = document.querySelectorAll('.video-tab-content'); + + if (tabButtons.length === 0) return; // 탭이 없으면 종료 + + // 탭 버튼 클릭 이벤트 + tabButtons.forEach(button => { + button.addEventListener('click', () => { + const targetTab = button.getAttribute('data-tab'); + + // 모든 탭 버튼에서 active 클래스 제거 + tabButtons.forEach(btn => btn.classList.remove('active')); + // 클릭된 탭 버튼에 active 클래스 추가 + button.classList.add('active'); + + // 모든 탭 콘텐츠 숨기기 + tabContents.forEach(content => { + content.classList.remove('active'); + }); + + // 선택된 탭 콘텐츠 표시 + const activeContent = document.getElementById(targetTab); + if (activeContent) { + activeContent.classList.add('active'); + + // 탭 전환 애니메이션 + activeContent.style.opacity = '0'; + activeContent.style.transform = 'translateY(20px)'; + + setTimeout(() => { + activeContent.style.transition = 'all 0.3s ease'; + activeContent.style.opacity = '1'; + activeContent.style.transform = 'translateY(0)'; + }, 50); + } + }); + + // 키보드 접근성 + button.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + button.click(); + } + }); + }); + + // 파트너 로고 애니메이션 + const partnerItems = document.querySelectorAll('.partner-logo-item'); + if (partnerItems.length > 0) { + const partnerObserver = new IntersectionObserver((entries) => { + entries.forEach((entry, index) => { + if (entry.isIntersecting) { + setTimeout(() => { + entry.target.style.opacity = '1'; + entry.target.style.transform = 'translateY(0)'; + }, index * 100); + partnerObserver.unobserve(entry.target); + } + }); + }, { threshold: 0.1 }); + + partnerItems.forEach(item => { + item.style.opacity = '0'; + item.style.transform = 'translateY(30px)'; + item.style.transition = 'all 0.6s ease'; + partnerObserver.observe(item); + }); + } + + // 비디오 아이템 애니메이션 + const videoItems = document.querySelectorAll('.live-video-item, .shorts-video-item'); + if (videoItems.length > 0) { + const videoObserver = new IntersectionObserver((entries) => { + entries.forEach((entry, index) => { + if (entry.isIntersecting) { + setTimeout(() => { + entry.target.style.opacity = '1'; + entry.target.style.transform = 'translateY(0)'; + }, index * 150); + videoObserver.unobserve(entry.target); + } + }); + }, { threshold: 0.1 }); + + videoItems.forEach(item => { + item.style.opacity = '0'; + item.style.transform = 'translateY(30px)'; + item.style.transition = 'all 0.6s ease'; + videoObserver.observe(item); + }); + } +} + +// ======================================== +// 비디오 레이지 로딩 +// ======================================== +function initVideoLazyLoading() { + const videoWrappers = document.querySelectorAll('.video-wrapper'); + + // Intersection Observer를 사용한 레이지 로딩 + const observerOptions = { + threshold: 0.1, + rootMargin: '50px 0px' + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + loadVideo(entry.target); + observer.unobserve(entry.target); + } + }); + }, observerOptions); + + videoWrappers.forEach(wrapper => { + observer.observe(wrapper); + + // 로딩 상태 표시 + if (!wrapper.querySelector('.video-loading')) { + const loadingDiv = document.createElement('div'); + loadingDiv.className = 'video-loading'; + loadingDiv.textContent = '비디오 로딩 중...'; + loadingDiv.style.cssText = ` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--text-secondary); + font-size: var(--font-sm); + z-index: 10; + `; + wrapper.appendChild(loadingDiv); + } + }); +} + +// 비디오 로딩 +function loadVideo(wrapper) { + const iframe = wrapper.querySelector('iframe'); + if (iframe && iframe.dataset.src) { + // 실제 src 설정 + iframe.src = iframe.dataset.src; + + // 로딩 완료 처리 + iframe.addEventListener('load', function() { + wrapper.classList.add('loaded'); + const loadingDiv = wrapper.querySelector('.video-loading'); + if (loadingDiv) { + loadingDiv.remove(); + } + }); + + // 에러 처리 + iframe.addEventListener('error', function() { + const loadingDiv = wrapper.querySelector('.video-loading'); + if (loadingDiv) { + loadingDiv.textContent = '비디오를 로드할 수 없습니다'; + loadingDiv.style.color = '#ef4444'; + } + }); + } else { + // 이미 src가 설정된 경우 + wrapper.classList.add('loaded'); + const loadingDiv = wrapper.querySelector('.video-loading'); + if (loadingDiv) { + loadingDiv.remove(); + } + } +} + // ======================================== // 마우스 호버 효과 // ======================================== diff --git a/js/portfolio.js b/js/portfolio.js index 2754e7e..08527e1 100644 --- a/js/portfolio.js +++ b/js/portfolio.js @@ -3,12 +3,59 @@ // ======================================== document.addEventListener('DOMContentLoaded', function() { + initPortfolioTabs(); initVideoLazyLoading(); initPortfolioAnimations(); initVideoInteractions(); initYouTubeAPI(); }); +// ======================================== +// 포트폴리오 탭 시스템 +// ======================================== +function initPortfolioTabs() { + const tabButtons = document.querySelectorAll('.tab-btn'); + const tabContents = document.querySelectorAll('.tab-content'); + + // 탭 버튼 클릭 이벤트 + tabButtons.forEach(button => { + button.addEventListener('click', () => { + const targetTab = button.getAttribute('data-tab'); + + // 모든 탭 버튼에서 active 클래스 제거 + tabButtons.forEach(btn => btn.classList.remove('active')); + // 클릭된 탭 버튼에 active 클래스 추가 + button.classList.add('active'); + + // 모든 탭 콘텐츠 숨기기 + tabContents.forEach(content => { + content.classList.remove('active'); + }); + + // 선택된 탭 콘텐츠 표시 + const activeContent = document.getElementById(targetTab); + if (activeContent) { + activeContent.classList.add('active'); + + // 탭 전환 시 비디오 레이지 로딩 재초기화 + setTimeout(() => { + initVideoLazyLoading(); + }, 100); + } + }); + }); + + // 키보드 접근성 + tabButtons.forEach(button => { + button.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + button.click(); + } + }); + }); +} + // 비디오 레이지 로딩 초기화 function initVideoLazyLoading() { const videoWrappers = document.querySelectorAll('.video-wrapper'); diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb6033f --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "mingle-studio-website", + "version": "1.0.0", + "description": "밍글 스튜디오 공식 웹사이트 - 인천 모션캡쳐 전문 스튜디오", + "main": "index.html", + "scripts": { + "dev": "npx serve . --listen 8000 --no-clipboard --no-compression --cors", + "start": "npx serve . --listen 8000 --no-clipboard --no-compression --cors", + "serve": "npx serve . --listen 8000 --no-clipboard --no-compression --cors", + "preview": "python -m http.server 8000", + "python": "python server.py", + "test": "echo \"웹사이트를 http://localhost:8000 에서 확인하세요\" && npm run dev" + }, + "keywords": [ + "motion-capture", + "studio", + "optitrack", + "vtuber", + "3d-animation" + ], + "author": "밍글 스튜디오", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/mingle-studio/website" + }, + "devDependencies": { + "serve": "^14.2.0" + } +} \ No newline at end of file diff --git a/portfolio.html b/portfolio.html index 2242f22..b5a2b53 100644 --- a/portfolio.html +++ b/portfolio.html @@ -55,7 +55,7 @@
- -
+ +
-
-

Long-Form 콘텐츠

-

밍글 스튜디오의 주요 모션캡쳐 프로젝트

+
+
+ + +
+
+
+ + +
+ +
+
+
+

Long-Form 콘텐츠

+

개인 크리에이터들의 모션캡쳐 프로젝트

+
+
@@ -211,6 +226,7 @@

Memory 깡담비 #하이라이트 #shorts

+ @@ -307,9 +323,130 @@ + - + + + + +
+
+
+
+

기업 프로젝트

+

기업 및 상업적 모션캡쳐 프로젝트

+
+ + +
+
+ +
+ 기업 프로젝트 + 버추얼 아이돌 + 엔터테인먼트 +
+
+ + +
+

뮤직비디오 제작

+
+ +
+
+ + +
+

숏폼비디오 제작

+
+
+
+ +
+
+

✨꼭꼭 숨어라 릴레이 댄스✨

+

아이시아랑 숨바꼭질 할 사람 🙋‍♀️🙋‍♀️

+
+
+ +
+
+ +
+
+

루화가 제일 싫어하는 말은❓

+

착하다는 말... 제일 싫어 💢💢

+
+
+
+
+ + +
+

라이브 방송 진행

+
+
+
+ +
+
+

✨IXIA 데뷔 라이브 쇼케이스✨

+

아이시아 데뷔 쇼케이스: TIME TO SAY IXIA

+
+ YouTube + 아이시아 +
+
+
+ +
+
+ +
+
+

🎤 아이시아의 라이브룸

+

플레이리스트 대공개 라이브 방송

+
+ YouTube + 아이시아 +
+
+
+
+
+
+
+
+
+
@@ -353,8 +490,8 @@

당신의 콘텐츠도 여기에

밍글 스튜디오와 함께 다음 포트폴리오의 주인공이 되어보세요

diff --git a/server.py b/server.py new file mode 100644 index 0000000..9eedb53 --- /dev/null +++ b/server.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +밍글 스튜디오 웹사이트 로컬 개발 서버 +Python 기반 HTTP 서버로 개발 및 테스트 지원 +""" + +import http.server +import socketserver +import webbrowser +import os +import sys +import socket +from pathlib import Path + +# 한글 출력을 위한 인코딩 설정 +if os.name == 'nt': # Windows + import locale + import codecs + # UTF-8 강제 설정 + try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') + except: + # Python 3.6 이하 버전 호환 + sys.stdout = codecs.getwriter('utf-8')(sys.stdout.detach()) + sys.stderr = codecs.getwriter('utf-8')(sys.stderr.detach()) + +# 서버 설정 +PORT = 3000 +HOST = '0.0.0.0' # 모든 인터페이스에서 접근 가능 + +class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + """커스텀 HTTP 요청 핸들러""" + + def end_headers(self): + # CORS 헤더 추가 (개발용) + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type') + # 캐시 비활성화 (개발용) + self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') + self.send_header('Pragma', 'no-cache') + self.send_header('Expires', '0') + super().end_headers() + + def do_GET(self): + """GET 요청 처리""" + original_path = self.path + + # .html로 직접 접근하는 경우 리다이렉트 + if self.path.endswith('.html') and self.path != '/index.html': + clean_path = self.path[:-5] # .html 제거 + self.send_response(301) + self.send_header('Location', clean_path) + self.end_headers() + return + + # .html 확장자 없이 접근 시 자동으로 .html 추가 + if not self.path.endswith('/') and '.' not in os.path.basename(self.path): + html_path = self.path + '.html' + if os.path.exists(html_path.lstrip('/')): + self.path = html_path + + # 기본 파일 처리 + if self.path == '/': + self.path = '/index.html' + + super().do_GET() + + def log_message(self, format, *args): + """로그 메시지 포맷팅""" + print(f"[{self.log_date_time_string()}] {format % args}") + +def find_available_port(start_port=8001): + """사용 가능한 포트 찾기""" + for port in range(start_port, start_port + 20): # 더 많은 포트 시도 + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 주소 재사용 + sock.bind(('localhost', port)) # 실제로 바인드 테스트 + sock.close() + print(f"Available port found: {port}") + return port # 성공하면 해당 포트 반환 + except OSError: + continue # 포트가 사용 중이면 다음 포트 시도 + + print(f"No available port found in range {start_port}-{start_port+19}") + return start_port # 찾지 못하면 기본값 반환 + +def main(): + """메인 서버 실행 함수""" + # 현재 디렉토리가 프로젝트 루트인지 확인 + if not os.path.exists('index.html'): + print("Error: index.html file not found.") + print("Please run from project root directory.") + sys.exit(1) + + # 사용 가능한 포트 찾기 + available_port = find_available_port(PORT) + + try: + # 서버 시작 + httpd = socketserver.TCPServer((HOST, available_port), CustomHTTPRequestHandler) + httpd.allow_reuse_address = True # 주소 재사용 허용 + with httpd: + # 로컬 및 외부 접근 주소 표시 + import socket + local_ip = socket.gethostbyname(socket.gethostname()) + + print("Mingle Studio Development Server Started!") + print("="*60) + print(f"Local Access: http://localhost:{available_port}") + print(f"Network Access: http://{local_ip}:{available_port}") + print(f"Root Directory: {os.getcwd()}") + print("="*60) + print("Main Pages:") + print(f" Home: http://localhost:{available_port}/") + print(f" About: http://localhost:{available_port}/about") + print(f" Services: http://localhost:{available_port}/services") + print(f" Portfolio: http://localhost:{available_port}/portfolio") + print(f" Gallery: http://localhost:{available_port}/gallery") + print(f" Contact: http://localhost:{available_port}/contact") + print(f" Q&A: http://localhost:{available_port}/qna") + print("="*60) + print("External Access Setup:") + print(f" 1. Setup port forwarding for port {available_port} on router") + print(f" 2. Access via public IP: http://[PUBLIC_IP]:{available_port}") + print(f" 3. Same network: http://{local_ip}:{available_port}") + print("="*60) + print("Tip: Press Ctrl+C to stop the server.") + print("Opening browser automatically...") + + # 기본 브라우저에서 자동으로 열기 (로컬호스트로) + webbrowser.open(f"http://localhost:{available_port}") + + # 서버 실행 + httpd.serve_forever() + + except KeyboardInterrupt: + print("\n\nServer stopped.") + print("Goodbye!") + except OSError as e: + if e.errno == 10048: # WinError 10048 + print(f"\nError: Port {available_port} is already in use.") + print("Try different port or check the following:") + print("1. Check if another server is running") + print("2. Kill Python process in Task Manager") + print(f"3. Run in command prompt: netstat -ano | findstr :{available_port}") + else: + print(f"Server error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file