diff --git a/emotion.d.ts b/emotion.d.ts
new file mode 100644
index 0000000..762b0d0
--- /dev/null
+++ b/emotion.d.ts
@@ -0,0 +1,15 @@
+import "@emotion/react";
+import { Theme as MuiTheme } from "@mui/material/styles";
+
+declare module "@mui/material/styles" {
+ interface PaletteColor {
+ nonFocus?: string;
+ }
+ interface SimplePaletteColorOptions {
+ nonFocus?: string;
+ }
+}
+
+declare module "@emotion/react" {
+ export interface Theme extends MuiTheme {}
+}
diff --git a/package.json b/package.json
index 489cc59..5933f99 100644
--- a/package.json
+++ b/package.json
@@ -31,12 +31,13 @@
"notistack": "^3.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "remeda": "^2.21.3"
+ "react-router-dom": "^7.5.3"
},
+
"devDependencies": {
"@eslint/js": "^9.21.0",
+ "@types/node": "^22.15.3",
"@tanstack/react-query-devtools": "^5.74.4",
- "@types/node": "^22.14.1",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@typescript-eslint/parser": "^8.29.1",
@@ -51,10 +52,13 @@
"globals": "^15.15.0",
"iamport-typings": "^1.4.0",
"prettier": "^3.5.3",
+ "remeda": "^2.21.3",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0",
- "vite-plugin-mdx": "^3.6.1"
+ "vite-plugin-mdx": "^3.6.1",
+ "vite-plugin-svgr": "^4.3.0"
+
},
"packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1abe734..ae2a479 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -59,6 +59,9 @@ importers:
react-dom:
specifier: ^19.0.0
version: 19.1.0(react@19.1.0)
+ react-router-dom:
+ specifier: ^7.5.3
+ version: 7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
remeda:
specifier: ^2.21.3
version: 2.21.3
@@ -66,12 +69,12 @@ importers:
'@eslint/js':
specifier: ^9.21.0
version: 9.24.0
+ '@types/node':
+ specifier: ^22.15.3
+ version: 22.15.3
'@tanstack/react-query-devtools':
specifier: ^5.74.4
version: 5.74.4(@tanstack/react-query@5.72.2(react@19.1.0))(react@19.1.0)
- '@types/node':
- specifier: ^22.14.1
- version: 22.14.1
'@types/react':
specifier: ^19.0.10
version: 19.1.1
@@ -83,10 +86,7 @@ importers:
version: 8.29.1(eslint@9.24.0)(typescript@5.7.3)
'@vitejs/plugin-react':
specifier: ^4.3.4
- version: 4.3.4(vite@6.2.6(@types/node@22.14.1))
- csstype:
- specifier: ^3.1.3
- version: 3.1.3
+ version: 4.3.4(vite@6.2.6(@types/node@22.15.3))
eslint:
specifier: ^9.21.0
version: 9.24.0
@@ -122,10 +122,13 @@ importers:
version: 8.29.1(eslint@9.24.0)(typescript@5.7.3)
vite:
specifier: ^6.2.0
- version: 6.2.6(@types/node@22.14.1)
+ version: 6.2.6(@types/node@22.15.3)
vite-plugin-mdx:
specifier: ^3.6.1
- version: 3.6.1(@mdx-js/mdx@3.1.0(acorn@8.14.1))(vite@6.2.6(@types/node@22.14.1))
+ version: 3.6.1(@mdx-js/mdx@3.1.0(acorn@8.14.1))(vite@6.2.6(@types/node@22.15.3))
+ vite-plugin-svgr:
+ specifier: ^4.3.0
+ version: 4.3.0(rollup@4.39.0)(typescript@5.7.3)(vite@6.2.6(@types/node@22.15.3))
packages:
@@ -729,6 +732,73 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+ '@svgr/babel-plugin-add-jsx-attribute@8.0.0':
+ resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-remove-jsx-attribute@8.0.0':
+ resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0':
+ resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0':
+ resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-svg-dynamic-title@8.0.0':
+ resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-svg-em-dimensions@8.0.0':
+ resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-transform-react-native-svg@8.1.0':
+ resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-plugin-transform-svg-component@8.0.0':
+ resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/babel-preset@8.1.0':
+ resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@svgr/core@8.1.0':
+ resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==}
+ engines: {node: '>=14'}
+
+ '@svgr/hast-util-to-babel-ast@8.0.0':
+ resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==}
+ engines: {node: '>=14'}
+
+ '@svgr/plugin-jsx@8.1.0':
+ resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@svgr/core': '*'
'@suspensive/react@2.18.12':
resolution: {integrity: sha512-De3sVLxLnMpTSOfW3t3D8uh8+/bK8+L/mV8YRAwjW2PyR8BBe9+nctFRVO+ZCIFKUs7VPtnIXnb+5bKfBQ1vog==}
peerDependencies:
@@ -790,8 +860,8 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
- '@types/node@22.14.1':
- resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==}
+ '@types/node@22.15.3':
+ resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==}
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -1002,6 +1072,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
+ camelcase@6.3.0:
+ resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+ engines: {node: '>=10'}
+
caniuse-lite@1.0.30001713:
resolution: {integrity: sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==}
@@ -1065,10 +1139,23 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie@1.0.2:
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+ engines: {node: '>=18'}
+
cosmiconfig@7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
engines: {node: '>=10'}
+ cosmiconfig@8.3.6:
+ resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1144,6 +1231,9 @@ packages:
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+ dot-case@3.0.4:
+ resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1157,6 +1247,10 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@@ -1862,6 +1956,9 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
+ lower-case@2.0.2:
+ resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -2025,6 +2122,9 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ no-case@3.0.4:
+ resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
+
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@@ -2184,6 +2284,23 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
+ react-router-dom@7.5.3:
+ resolution: {integrity: sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.5.3:
+ resolution: {integrity: sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@@ -2277,6 +2394,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -2317,6 +2437,9 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
+ snake-case@3.0.4:
+ resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -2380,6 +2503,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ svg-parser@2.0.4:
+ resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
+
synckit@0.11.3:
resolution: {integrity: sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -2413,6 +2539,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ turbo-stream@2.4.0:
+ resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -2514,6 +2643,11 @@ packages:
'@mdx-js/mdx': <2
vite: <3
+ vite-plugin-svgr@4.3.0:
+ resolution: {integrity: sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==}
+ peerDependencies:
+ vite: '>=2.6.0'
+
vite@6.2.6:
resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -3164,6 +3298,75 @@ snapshots:
'@rtsao/scc@1.1.0': {}
+ '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.10)':
+ dependencies:
+ '@babel/core': 7.26.10
+
+ '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.26.10)':
+ dependencies:
+ '@babel/core': 7.26.10
+
+ '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.26.10)':
+ dependencies:
+ '@babel/core': 7.26.10
+
+ '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.26.10)':
+ dependencies:
+ '@babel/core': 7.26.10
+
+ '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.26.10)':
+ dependencies:
+ '@babel/core': 7.26.10
+
+ '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.26.10)':
+ dependencies:
+ '@babel/core': 7.26.10
+
+ '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.26.10)':
+ dependencies:
+ '@babel/core': 7.26.10
+
+ '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.26.10)':
+ dependencies:
+ '@babel/core': 7.26.10
+
+ '@svgr/babel-preset@8.1.0(@babel/core@7.26.10)':
+ dependencies:
+ '@babel/core': 7.26.10
+ '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.26.10)
+ '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.26.10)
+ '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.26.10)
+ '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.26.10)
+ '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.26.10)
+ '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.26.10)
+ '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.26.10)
+ '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.26.10)
+
+ '@svgr/core@8.1.0(typescript@5.7.3)':
+ dependencies:
+ '@babel/core': 7.26.10
+ '@svgr/babel-preset': 8.1.0(@babel/core@7.26.10)
+ camelcase: 6.3.0
+ cosmiconfig: 8.3.6(typescript@5.7.3)
+ snake-case: 3.0.4
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@svgr/hast-util-to-babel-ast@8.0.0':
+ dependencies:
+ '@babel/types': 7.27.0
+ entities: 4.5.0
+
+ '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.7.3))':
+ dependencies:
+ '@babel/core': 7.26.10
+ '@svgr/babel-preset': 8.1.0(@babel/core@7.26.10)
+ '@svgr/core': 8.1.0(typescript@5.7.3)
+ '@svgr/hast-util-to-babel-ast': 8.0.0
+ svg-parser: 2.0.4
+ transitivePeerDependencies:
+ - supports-color
'@suspensive/react@2.18.12(react@19.1.0)':
dependencies:
react: 19.1.0
@@ -3230,7 +3433,7 @@ snapshots:
'@types/ms@2.1.0': {}
- '@types/node@22.14.1':
+ '@types/node@22.15.3':
dependencies:
undici-types: 6.21.0
@@ -3333,14 +3536,14 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
- '@vitejs/plugin-react@4.3.4(vite@6.2.6(@types/node@22.14.1))':
+ '@vitejs/plugin-react@4.3.4(vite@6.2.6(@types/node@22.15.3))':
dependencies:
'@babel/core': 7.26.10
'@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10)
'@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10)
'@types/babel__core': 7.20.5
react-refresh: 0.14.2
- vite: 6.2.6(@types/node@22.14.1)
+ vite: 6.2.6(@types/node@22.15.3)
transitivePeerDependencies:
- supports-color
@@ -3492,6 +3695,8 @@ snapshots:
callsites@3.1.0: {}
+ camelcase@6.3.0: {}
+
caniuse-lite@1.0.30001713: {}
ccount@2.0.1: {}
@@ -3537,6 +3742,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie@1.0.2: {}
+
cosmiconfig@7.1.0:
dependencies:
'@types/parse-json': 4.0.2
@@ -3545,6 +3752,15 @@ snapshots:
path-type: 4.0.0
yaml: 1.10.2
+ cosmiconfig@8.3.6(typescript@5.7.3):
+ dependencies:
+ import-fresh: 3.3.1
+ js-yaml: 4.1.0
+ parse-json: 5.2.0
+ path-type: 4.0.0
+ optionalDependencies:
+ typescript: 5.7.3
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -3620,6 +3836,11 @@ snapshots:
'@babel/runtime': 7.27.0
csstype: 3.1.3
+ dot-case@3.0.4:
+ dependencies:
+ no-case: 3.0.4
+ tslib: 2.8.1
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -3632,6 +3853,8 @@ snapshots:
emoji-regex@9.2.2: {}
+ entities@4.5.0: {}
+
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
@@ -4481,6 +4704,10 @@ snapshots:
dependencies:
js-tokens: 4.0.0
+ lower-case@2.0.2:
+ dependencies:
+ tslib: 2.8.1
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -4827,6 +5054,11 @@ snapshots:
natural-compare@1.4.0: {}
+ no-case@3.0.4:
+ dependencies:
+ lower-case: 2.0.2
+ tslib: 2.8.1
+
node-releases@2.0.19: {}
notistack@3.0.2(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
@@ -4986,6 +5218,21 @@ snapshots:
react-refresh@0.14.2: {}
+ react-router-dom@7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ react-router: 7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+
+ react-router@7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ cookie: 1.0.2
+ react: 19.1.0
+ set-cookie-parser: 2.7.1
+ turbo-stream: 2.4.0
+ optionalDependencies:
+ react-dom: 19.1.0(react@19.1.0)
+
react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@babel/runtime': 7.27.0
@@ -5150,6 +5397,8 @@ snapshots:
semver@7.7.1: {}
+ set-cookie-parser@2.7.1: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -5208,6 +5457,11 @@ snapshots:
slash@3.0.0: {}
+ snake-case@3.0.4:
+ dependencies:
+ dot-case: 3.0.4
+ tslib: 2.8.1
+
source-map-js@1.2.1: {}
source-map@0.5.7: {}
@@ -5274,6 +5528,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ svg-parser@2.0.4: {}
+
synckit@0.11.3:
dependencies:
'@pkgr/core': 0.2.2
@@ -5306,6 +5562,8 @@ snapshots:
tslib@2.8.1: {}
+ turbo-stream@2.4.0: {}
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -5451,22 +5709,33 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
- vite-plugin-mdx@3.6.1(@mdx-js/mdx@3.1.0(acorn@8.14.1))(vite@6.2.6(@types/node@22.14.1)):
+ vite-plugin-mdx@3.6.1(@mdx-js/mdx@3.1.0(acorn@8.14.1))(vite@6.2.6(@types/node@22.15.3)):
dependencies:
'@alloc/quick-lru': 5.2.0
'@mdx-js/mdx': 3.1.0(acorn@8.14.1)
esbuild: 0.13.8
resolve: 1.22.10
unified: 9.2.2
- vite: 6.2.6(@types/node@22.14.1)
+ vite: 6.2.6(@types/node@22.15.3)
+
+ vite-plugin-svgr@4.3.0(rollup@4.39.0)(typescript@5.7.3)(vite@6.2.6(@types/node@22.15.3)):
+ dependencies:
+ '@rollup/pluginutils': 5.1.4(rollup@4.39.0)
+ '@svgr/core': 8.1.0(typescript@5.7.3)
+ '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.7.3))
+ vite: 6.2.6(@types/node@22.15.3)
+ transitivePeerDependencies:
+ - rollup
+ - supports-color
+ - typescript
- vite@6.2.6(@types/node@22.14.1):
+ vite@6.2.6(@types/node@22.15.3):
dependencies:
esbuild: 0.25.2
postcss: 8.5.3
rollup: 4.39.0
optionalDependencies:
- '@types/node': 22.14.1
+ '@types/node': 22.15.3
fsevents: 2.3.3
which-boxed-primitive@1.1.1:
diff --git a/src/App.tsx b/src/App.tsx
index b7ec56c..a4ae52e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,17 @@
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import MainLayout from "./components/layout";
import Test from "./components/Test";
function App() {
- return ;
+ return (
+
+
+ }>
+ } />
+
+
+
+ );
}
export default App;
diff --git a/src/assets/Footer/blog.svg b/src/assets/Footer/blog.svg
new file mode 100644
index 0000000..ecb62c2
--- /dev/null
+++ b/src/assets/Footer/blog.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/Footer/facebook.svg b/src/assets/Footer/facebook.svg
new file mode 100644
index 0000000..95bbb43
--- /dev/null
+++ b/src/assets/Footer/facebook.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/Footer/flickr.svg b/src/assets/Footer/flickr.svg
new file mode 100644
index 0000000..cae2f99
--- /dev/null
+++ b/src/assets/Footer/flickr.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/Footer/github.svg b/src/assets/Footer/github.svg
new file mode 100644
index 0000000..d429ea8
--- /dev/null
+++ b/src/assets/Footer/github.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/Footer/instagram.svg b/src/assets/Footer/instagram.svg
new file mode 100644
index 0000000..dd65c88
--- /dev/null
+++ b/src/assets/Footer/instagram.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/Footer/linkedin.svg b/src/assets/Footer/linkedin.svg
new file mode 100644
index 0000000..7e2ad8e
--- /dev/null
+++ b/src/assets/Footer/linkedin.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/Footer/message.svg b/src/assets/Footer/message.svg
new file mode 100644
index 0000000..69646e0
--- /dev/null
+++ b/src/assets/Footer/message.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/Footer/x.svg b/src/assets/Footer/x.svg
new file mode 100644
index 0000000..8d47316
--- /dev/null
+++ b/src/assets/Footer/x.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/Footer/youtube.svg b/src/assets/Footer/youtube.svg
new file mode 100644
index 0000000..d130f81
--- /dev/null
+++ b/src/assets/Footer/youtube.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/langIcon.png b/src/assets/langIcon.png
new file mode 100644
index 0000000..1157c55
Binary files /dev/null and b/src/assets/langIcon.png differ
diff --git a/src/assets/pyconLogo.png b/src/assets/pyconLogo.png
new file mode 100644
index 0000000..b1d5c7b
Binary files /dev/null and b/src/assets/pyconLogo.png differ
diff --git a/src/components/layout/Footer/index.tsx b/src/components/layout/Footer/index.tsx
new file mode 100644
index 0000000..c2f1031
--- /dev/null
+++ b/src/components/layout/Footer/index.tsx
@@ -0,0 +1,175 @@
+import styled from "@emotion/styled";
+import { useEmail } from "@/hooks/useEmail";
+import MessageIcon from "@/assets/Footer/message.svg?react";
+import FacebookIcon from "@/assets/Footer/facebook.svg?react";
+import YoutubeIcon from "@/assets/Footer/youtube.svg?react";
+import XIcon from "@/assets/Footer/x.svg?react";
+import GithubIcon from "@/assets/Footer/github.svg?react";
+import InstagramIcon from "@/assets/Footer/instagram.svg?react";
+import LinkedinIcon from "@/assets/Footer/linkedin.svg?react";
+import BlogIcon from "@/assets/Footer/blog.svg?react";
+import FlickrIcon from "@/assets/Footer/flickr.svg?react";
+
+interface LinkItem {
+ text: string;
+ href: string;
+}
+
+interface IconItem {
+ icon: React.FC>;
+ alt: string;
+ href: string;
+}
+
+interface FooterProps {
+ slogan?: string;
+ description?: string;
+ links?: LinkItem[];
+ icons?: IconItem[];
+}
+
+const defaultIcons: IconItem[] = [
+ {
+ icon: FacebookIcon,
+ alt: "facebook",
+ href: "https://www.facebook.com/pyconkorea/",
+ },
+ {
+ icon: YoutubeIcon,
+ alt: "youtube",
+ href: "https://www.youtube.com/c/PyConKRtube",
+ },
+ { icon: XIcon, alt: "x", href: "https://x.com/PyConKR" },
+ { icon: GithubIcon, alt: "github", href: "https://github.com/pythonkr" },
+ {
+ icon: InstagramIcon,
+ alt: "instagram",
+ href: "https://www.instagram.com/pycon_korea/",
+ },
+ {
+ icon: LinkedinIcon,
+ alt: "linkedin",
+ href: "https://www.linkedin.com/company/pyconkorea/",
+ },
+ { icon: BlogIcon, alt: "blog", href: "https://blog.pycon.kr/" },
+ {
+ icon: FlickrIcon,
+ alt: "flickr",
+ href: "https://www.flickr.com/photos/126829363@N08/",
+ },
+];
+
+export default function Footer({
+ slogan = "Weave with Python, 파이콘 한국 2025",
+ description = "파이콘 한국 2025는 파이콘 한국 준비위원회가 만들고 있습니다\n파이썬 웹 프레임워크 Django로 만들었습니다",
+ links = [
+ { text: "파이콘 한국 행동 강령(CoC)", href: "#" },
+ { text: "서비스 이용 약관", href: "#" },
+ { text: "개인 정보 처리 방침", href: "#" },
+ ],
+ icons = defaultIcons,
+}: FooterProps) {
+ const { sendEmail } = useEmail();
+
+ return (
+
+
+ {slogan}
+ {description.split("\n").map((line, index) => (
+ {line}
+ ))}
+
+ {links.map((link, index) => (
+ <>
+
+ {link.text}
+
+ {index < links.length - 1 && |}
+ >
+ ))}
+
+
+
+
+
+ {icons.map((icon) => (
+
+
+
+ ))}
+
+
+
+ );
+}
+
+const FooterContainer = styled.footer`
+ background-color: ${({ theme }) => theme.palette.primary.main};
+ color: ${({ theme }) => theme.palette.common.white};
+ font-size: 0.75rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 267px;
+`;
+
+const FooterContent = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.75rem;
+`;
+
+const FooterSlogan = styled.div`
+ font-weight: 600;
+`;
+
+const FooterLinks = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+`;
+const FooterIcons = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 9px;
+`;
+
+const Link = styled.a`
+ color: ${({ theme }) => theme.palette.common.white};
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+`;
+
+const Separator = styled.span`
+ color: ${({ theme }) => theme.palette.common.white};
+ opacity: 0.5;
+`;
+
+const IconLink = styled.a`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ cursor: pointer;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ img {
+ width: 20px;
+ height: 20px;
+ }
+`;
diff --git a/src/components/layout/Header/index.tsx b/src/components/layout/Header/index.tsx
new file mode 100644
index 0000000..7e79de1
--- /dev/null
+++ b/src/components/layout/Header/index.tsx
@@ -0,0 +1,170 @@
+import styled from "@emotion/styled";
+import { useMenu } from "../../../hooks/useMenu";
+import LanguageSelector from "../LanguageSelector";
+import LoginButton from "../LoginButton";
+
+interface SubMenuItem {
+ text: string;
+ href: string;
+}
+
+interface MenuItem {
+ text: string;
+ href?: string;
+ subMenu?: SubMenuItem[];
+}
+
+interface HeaderProps {
+ menus: MenuItem[];
+}
+
+export default function Header({ menus }: HeaderProps) {
+ const {
+ hoveredMenu,
+ focusedMenu,
+ menuRefs,
+ setHoveredMenu,
+ setFocusedMenu,
+ handleKeyDown,
+ handleBlur,
+ } = useMenu();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const HeaderContainer = styled.header`
+ background-color: ${({ theme }) => theme.palette.primary.light};
+ color: ${({ theme }) => theme.palette.primary.dark};
+ font-size: 0.8125rem;
+ font-weight: 500;
+ width: 100%;
+ height: 3.625rem;
+ padding: 0.5625rem 7.125rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ position: relative;
+`;
+
+const HeaderLogo = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
+const HeaderNav = styled.ul`
+ display: flex;
+ align-items: center;
+ gap: 2rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ position: relative;
+
+ li {
+ position: relative;
+ cursor: pointer;
+ outline: none;
+
+ &:focus {
+ outline: 2px solid ${({ theme }) => theme.palette.primary.main};
+ outline-offset: 1px;
+ }
+ }
+`;
+
+const HeaderLeft = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 1.125rem;
+`;
+
+// const HeaderItem = styled.div`
+// display: flex;
+// align-items: center;
+// gap: 0.625rem;
+// `;
+
+const SubMenu = styled.ul`
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: white;
+ border-radius: 5px;
+ padding: 5px 0;
+ width: 125px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+`;
+
+const SubMenuItem = styled.li`
+ padding: 5px 0;
+ text-align: center;
+
+ a {
+ color: ${({ theme }) => theme.palette.primary.light};
+ text-decoration: none;
+ font-size: 10px;
+ display: block;
+ outline: none;
+
+ &:hover,
+ &:focus {
+ color: ${({ theme }) => theme.palette.primary.main};
+ font-weight: 600;
+ }
+
+ &:focus {
+ outline: 2px solid ${({ theme }) => theme.palette.primary.main};
+ outline-offset: 0.5px;
+ }
+ }
+`;
diff --git a/src/components/layout/LanguageSelector/index.tsx b/src/components/layout/LanguageSelector/index.tsx
new file mode 100644
index 0000000..d0a58e8
--- /dev/null
+++ b/src/components/layout/LanguageSelector/index.tsx
@@ -0,0 +1,42 @@
+import styled from "@emotion/styled";
+import { useState } from "react";
+
+export default function LanguageSelector() {
+ const [selectedLang, setSelectedLang] = useState<"KO" | "EN">("KO");
+
+ return (
+
+
+ setSelectedLang("KO")}
+ >
+ KO
+
+ setSelectedLang("EN")}
+ >
+ EN
+
+
+ );
+}
+
+const LanguageContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+`;
+
+const LanguageItem = styled.div<{ isSelected: boolean }>`
+ cursor: pointer;
+ color: ${({ isSelected, theme }) =>
+ isSelected ? theme.palette.primary.dark : theme.palette.primary.nonFocus};
+ transition: color 0.2s ease;
+`;
diff --git a/src/components/layout/LoginButton/index.tsx b/src/components/layout/LoginButton/index.tsx
new file mode 100644
index 0000000..7b8b17d
--- /dev/null
+++ b/src/components/layout/LoginButton/index.tsx
@@ -0,0 +1,16 @@
+import styled from "@emotion/styled";
+
+export default function LoginButton() {
+ return 로그인;
+}
+
+const LoginButtonStyled = styled.button`
+ background: none;
+ border: none;
+ color: ${({ theme }) => theme.palette.primary.dark};
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ padding: 0;
+ transition: color 0.2s ease;
+`;
diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx
new file mode 100644
index 0000000..3af9663
--- /dev/null
+++ b/src/components/layout/index.tsx
@@ -0,0 +1,69 @@
+import { Outlet } from "react-router-dom";
+import Header from "./Header";
+import Footer from "./Footer";
+import styled from "@emotion/styled";
+
+const headerMenus = [
+ {
+ text: "파이콘 한국",
+ subMenu: [
+ { text: "파이콘 한국 2025", href: "/2025" },
+ { text: "파이콘 한국 행동강령(CoC)", href: "/coc" },
+ { text: "파이썬 사용자 모임", href: "/user-group" },
+ { text: "역대 파이콘 행사", href: "/past-events" },
+ { text: "파이콘 한국 건강 관련 안내", href: "/health" },
+ ],
+ },
+ {
+ text: "프로그램",
+ subMenu: [
+ { text: "튜토리얼", href: "/tutorial" },
+ { text: "스프린트", href: "/sprint" },
+ { text: "포스터 세션", href: "/poster" },
+ ],
+ },
+ {
+ text: "세션",
+ subMenu: [
+ { text: "세션 목록", href: "/sessions" },
+ { text: "세션 시간표", href: "/schedule" },
+ ],
+ },
+ {
+ text: "구매",
+ subMenu: [
+ { text: "티켓 구매", href: "/tickets" },
+ { text: "굿즈 구매", href: "/goods" },
+ { text: "결제 내역", href: "/payments" },
+ ],
+ },
+ {
+ text: "후원하기",
+ subMenu: [
+ { text: "후원사 안내", href: "/sponsors" },
+ { text: "개인 후원자", href: "/individual-sponsors" },
+ ],
+ },
+];
+
+const LayoutContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+`;
+
+const MainContent = styled.main`
+ flex: 1;
+`;
+
+export default function MainLayout() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/hooks/useEmail.ts b/src/hooks/useEmail.ts
new file mode 100644
index 0000000..136d257
--- /dev/null
+++ b/src/hooks/useEmail.ts
@@ -0,0 +1,11 @@
+export const useEmail = () => {
+ const sendEmail = () => {
+ const email = "pyconkr@pycon.kr";
+ const subject = "파이콘 한국 문의";
+ const body = "";
+
+ window.location.href = `mailto:${email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
+ };
+
+ return { sendEmail };
+};
diff --git a/src/hooks/useMenu.ts b/src/hooks/useMenu.ts
new file mode 100644
index 0000000..fffc6c7
--- /dev/null
+++ b/src/hooks/useMenu.ts
@@ -0,0 +1,55 @@
+import { useState, useRef, useEffect } from "react";
+
+interface MenuItem {
+ text: string;
+ href?: string;
+ subMenu?: {
+ text: string;
+ href: string;
+ }[];
+}
+
+export const useMenu = () => {
+ const [hoveredMenu, setHoveredMenu] = useState(null);
+ const [focusedMenu, setFocusedMenu] = useState(null);
+ const menuRefs = useRef<{ [key: string]: HTMLLIElement | null }>({});
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ focusedMenu &&
+ !menuRefs.current[focusedMenu]?.contains(event.target as Node)
+ ) {
+ setFocusedMenu(null);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, [focusedMenu]);
+
+ const handleKeyDown = (e: React.KeyboardEvent, menu: MenuItem) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ setFocusedMenu(menu.text);
+ }
+ };
+
+ const handleBlur = (menu: MenuItem) => {
+ setTimeout(() => {
+ if (!menuRefs.current[menu.text]?.contains(document.activeElement)) {
+ setFocusedMenu(null);
+ }
+ }, 0);
+ };
+
+ return {
+ hoveredMenu,
+ focusedMenu,
+ menuRefs,
+ setHoveredMenu,
+ setFocusedMenu,
+ handleKeyDown,
+ handleBlur,
+ };
+};
diff --git a/src/main.tsx b/src/main.tsx
index e6a6703..edcb36c 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,7 +2,8 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider, CssBaseline } from "@mui/material";
-import { theme } from "./theme";
+import { Global } from "@emotion/react";
+import { muiTheme, globalStyles } from "./styles/globalStyles";
import "./index.css";
import App from "./App.tsx";
@@ -19,8 +20,9 @@ const queryClient = new QueryClient({
createRoot(document.getElementById("root")!).render(
-
+
+
diff --git a/src/styles/globalStyles.ts b/src/styles/globalStyles.ts
new file mode 100644
index 0000000..37130ee
--- /dev/null
+++ b/src/styles/globalStyles.ts
@@ -0,0 +1,102 @@
+import { css } from "@emotion/react";
+import { createTheme } from "@mui/material/styles";
+
+export const muiTheme = createTheme({
+ typography: {
+ fontFamily:
+ 'Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif',
+ },
+ palette: {
+ primary: {
+ main: "#259299",
+ light: "#B6D8D7",
+ dark: "#126D7F",
+ nonFocus: "#7AB2B3",
+ },
+ secondary: {
+ main: "#259299",
+ light: "#B6D8D7",
+ dark: "#126D7F",
+ },
+ text: {
+ primary: "#000000",
+ secondary: "#666666",
+ disabled: "#999999",
+ },
+ background: {
+ default: "#FFFFFF",
+ paper: "#FFFFFF",
+ },
+ common: {
+ black: "#000000",
+ white: "#FFFFFF",
+ },
+ error: {
+ main: "#d32f2f",
+ light: "#ef5350",
+ dark: "#c62828",
+ },
+ warning: {
+ main: "#ed6c02",
+ light: "#ff9800",
+ dark: "#e65100",
+ },
+ info: {
+ main: "#0288d1",
+ light: "#03a9f4",
+ dark: "#01579b",
+ },
+ success: {
+ main: "#2e7d32",
+ light: "#4caf50",
+ dark: "#1b5e20",
+ },
+ },
+});
+
+export const globalStyles = css`
+ @import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css");
+
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ html,
+ body {
+ font-family:
+ "Pretendard",
+ -apple-system,
+ BlinkMacSystemFont,
+ system-ui,
+ Roboto,
+ "Helvetica Neue",
+ "Segoe UI",
+ "Apple SD Gothic Neo",
+ "Noto Sans KR",
+ "Malgun Gothic",
+ "Apple Color Emoji",
+ "Segoe UI Emoji",
+ "Segoe UI Symbol",
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+
+ a {
+ text-decoration: none;
+ color: inherit;
+ }
+
+ button {
+ border: none;
+ background: none;
+ cursor: pointer;
+ }
+
+ ul,
+ ol {
+ list-style: none;
+ }
+`;
diff --git a/src/theme/index.ts b/src/theme/index.ts
index f6dde35..73a8311 100644
--- a/src/theme/index.ts
+++ b/src/theme/index.ts
@@ -2,12 +2,43 @@ import { createTheme } from "@mui/material";
export const theme = createTheme({
palette: {
- mode: "light",
primary: {
- main: "#1976d2",
+ main: "#259299",
+ light: "#B6D8D7",
+ dark: "#126D7F",
},
- secondary: {
- main: "#9c27b0",
+ text: {
+ primary: "#000000",
+ secondary: "#666666",
+ disabled: "#999999",
+ },
+ background: {
+ default: "#FFFFFF",
+ paper: "#FFFFFF",
+ },
+ common: {
+ black: "#000000",
+ white: "#FFFFFF",
+ },
+ error: {
+ main: "#d32f2f",
+ light: "#ef5350",
+ dark: "#c62828",
+ },
+ warning: {
+ main: "#ed6c02",
+ light: "#ff9800",
+ dark: "#e65100",
+ },
+ info: {
+ main: "#0288d1",
+ light: "#03a9f4",
+ dark: "#01579b",
+ },
+ success: {
+ main: "#2e7d32",
+ light: "#4caf50",
+ dark: "#1b5e20",
},
},
});
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 4b3ccaa..c3ecd2b 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1,4 +1,11 @@
///
+
+declare module "*.svg?react" {
+ import React = require("react");
+ const component: React.FC>;
+ export default component;
+}
+
interface ViteTypeOptions {
strictImportEnv: unknown;
}
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 5accaa5..ad0f6a5 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -21,7 +21,13 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
- "forceConsistentCasingInFileNames": false
+ "forceConsistentCasingInFileNames": false,
+
+ /* Paths */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
},
- "include": ["src", "package"]
+ "include": ["src", "package", "emotion.d.ts"]
}
diff --git a/vite.config.ts b/vite.config.ts
index 609fe35..1fe7815 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,18 +1,20 @@
import mdx from "@mdx-js/rollup";
import react from "@vitejs/plugin-react";
-import path from 'path';
+import path from "path";
import { defineConfig } from "vite";
+import svgr from "vite-plugin-svgr";
-// https://vite.dev/config/
+// https://vitejs.dev/config/
export default defineConfig({
base: "/frontend/",
envDir: "./dotenv",
- plugins: [react(), mdx()],
+ plugins: [react(), mdx(), svgr()],
resolve: {
alias: {
- '@pyconkr-common': path.resolve(__dirname, './package/pyconkr-common'),
- '@pyconkr-shop': path.resolve(__dirname, './package/pyconkr-shop'),
- '@src': path.resolve(__dirname, './src'),
+ "@pyconkr-common": path.resolve(__dirname, "./package/pyconkr-common"),
+ "@pyconkr-shop": path.resolve(__dirname, "./package/pyconkr-shop"),
+ "@src": path.resolve(__dirname, "./src"),
+ "@": path.resolve(__dirname, "./src"),
},
},
});