Compare commits
8 Commits
4d91da9b9a
...
b05c7bd080
| Author | SHA1 | Date |
|---|---|---|
|
|
b05c7bd080 | |
|
|
cd4114f74d | |
|
|
ed13c83ae0 | |
|
|
be32a04dd0 | |
|
|
cab67c711a | |
|
|
440a6c8b72 | |
|
|
ba4d143189 | |
|
|
1a4155da9b |
|
|
@ -1,6 +0,0 @@
|
||||||
# Local server
|
|
||||||
VITE_PORT=3000
|
|
||||||
VITE_HOST=localhost
|
|
||||||
|
|
||||||
# API server
|
|
||||||
VITE_API_URL=http://40.82.133.44
|
|
||||||
|
|
@ -38,6 +38,9 @@ CASTAD 프론트엔드 프로젝트 입니다.
|
||||||
# 의존성 설치
|
# 의존성 설치
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
|
# recharts 설치 (대시보드용 차트 라이브러리)
|
||||||
|
npm install recharts
|
||||||
|
|
||||||
# 개발 서버 실행 (http://localhost:3000)
|
# 개발 서버 실행 (http://localhost:3000)
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@
|
||||||
"i18next": "^25.8.4",
|
"i18next": "^25.8.4",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-i18next": "^16.5.4"
|
"react-i18next": "^16.5.4",
|
||||||
|
"recharts": "^3.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
|
@ -804,6 +805,42 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^11.0.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit/node_modules/immer": {
|
||||||
|
"version": "11.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
|
||||||
|
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.53",
|
"version": "1.0.0-beta.53",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
||||||
|
|
@ -1119,6 +1156,18 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|
@ -1164,6 +1213,69 @@
|
||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||||
|
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
|
@ -1182,6 +1294,12 @@
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
|
||||||
|
|
@ -1269,6 +1387,15 @@
|
||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
|
@ -1276,6 +1403,127 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
|
@ -1294,6 +1542,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.267",
|
"version": "1.5.267",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||||
|
|
@ -1301,6 +1555,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/es-toolkit": {
|
||||||
|
"version": "1.44.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
|
||||||
|
"integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"docs",
|
||||||
|
"benchmarks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
|
|
@ -1353,6 +1617,12 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
|
@ -1437,6 +1707,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|
@ -1578,6 +1867,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1612,6 +1902,37 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "19.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
|
||||||
|
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||||
|
|
@ -1622,6 +1943,58 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz",
|
||||||
|
"integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"www"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"es-toolkit": "^1.39.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"react-redux": "8.x.x || 9.x.x",
|
||||||
|
"reselect": "5.1.1",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"use-sync-external-store": "^1.2.2",
|
||||||
|
"victory-vendor": "^37.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.54.0",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
|
||||||
|
|
@ -1690,6 +2063,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
|
@ -1769,6 +2148,28 @@
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "37.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.4.1",
|
"version": "6.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
"i18next": "^25.8.4",
|
"i18next": "^25.8.4",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-i18next": "^16.5.4"
|
"react-i18next": "^16.5.4",
|
||||||
|
"recharts": "^3.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
const [isChannelDropdownOpen, setIsChannelDropdownOpen] = useState(false);
|
const [isChannelDropdownOpen, setIsChannelDropdownOpen] = useState(false);
|
||||||
const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false);
|
const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false);
|
||||||
const [isLoadingAutoDescription, setIsLoadingAutoDescription] = useState(false);
|
const [isLoadingAutoDescription, setIsLoadingAutoDescription] = useState(false);
|
||||||
|
const [isHorizontalVideo, setIsHorizontalVideo] = useState(false);
|
||||||
const channelDropdownRef = useRef<HTMLDivElement>(null);
|
const channelDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const privacyDropdownRef = useRef<HTMLDivElement>(null);
|
const privacyDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
@ -159,6 +160,16 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 모달 열릴 때 배경 스크롤 차단
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
return () => { document.body.style.overflow = ''; };
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
// 소셜 계정 로드
|
// 소셜 계정 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
|
@ -419,9 +430,13 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
<div className="social-posting-video-container">
|
<div className="social-posting-video-container">
|
||||||
<video
|
<video
|
||||||
src={video.result_movie_url}
|
src={video.result_movie_url}
|
||||||
className="social-posting-video"
|
className={`social-posting-video ${isHorizontalVideo ? 'horizontal' : 'vertical'}`}
|
||||||
controls
|
controls
|
||||||
playsInline
|
playsInline
|
||||||
|
onLoadedMetadata={(e) => {
|
||||||
|
const v = e.currentTarget;
|
||||||
|
setIsHorizontalVideo(v.videoWidth > v.videoHeight);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,11 @@
|
||||||
"imageUpload": "Image Upload",
|
"imageUpload": "Image Upload",
|
||||||
"dragAndDrop": "Drag and drop\nimages to upload",
|
"dragAndDrop": "Drag and drop\nimages to upload",
|
||||||
"videoRatio": "Video Ratio",
|
"videoRatio": "Video Ratio",
|
||||||
|
"minImages": "Min. 5 images",
|
||||||
|
"youtubeShorts": "YouTube Shorts",
|
||||||
|
"youtubeVideo": "YouTube Video",
|
||||||
|
"back": "Go Back",
|
||||||
|
"loadMore": "Load more",
|
||||||
"uploadFailed": "Image upload failed.",
|
"uploadFailed": "Image upload failed.",
|
||||||
"uploading": "Uploading...",
|
"uploading": "Uploading...",
|
||||||
"nextStep": "Next Step"
|
"nextStep": "Next Step"
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,11 @@
|
||||||
"imageUpload": "이미지 업로드",
|
"imageUpload": "이미지 업로드",
|
||||||
"dragAndDrop": "이미지를 드래그하여\n업로드",
|
"dragAndDrop": "이미지를 드래그하여\n업로드",
|
||||||
"videoRatio": "영상 비율",
|
"videoRatio": "영상 비율",
|
||||||
|
"minImages": "최소 5장",
|
||||||
|
"youtubeShorts": "유튜브 쇼츠",
|
||||||
|
"youtubeVideo": "유튜브 일반",
|
||||||
|
"back": "뒤로가기",
|
||||||
|
"loadMore": "더보기",
|
||||||
"uploadFailed": "이미지 업로드에 실패했습니다.",
|
"uploadFailed": "이미지 업로드에 실패했습니다.",
|
||||||
"uploading": "업로드 중...",
|
"uploading": "업로드 중...",
|
||||||
"nextStep": "다음 단계"
|
"nextStep": "다음 단계"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { uploadImages } from '../../utils/api';
|
||||||
|
|
||||||
interface AssetManagementContentProps {
|
interface AssetManagementContentProps {
|
||||||
onNext: (imageTaskId: string) => void;
|
onNext: (imageTaskId: string) => void;
|
||||||
|
onBack?: () => void;
|
||||||
imageList: ImageItem[];
|
imageList: ImageItem[];
|
||||||
onRemoveImage: (index: number) => void;
|
onRemoveImage: (index: number) => void;
|
||||||
onAddImages: (files: File[]) => void;
|
onAddImages: (files: File[]) => void;
|
||||||
|
|
@ -13,20 +14,22 @@ interface AssetManagementContentProps {
|
||||||
|
|
||||||
type VideoRatio = 'vertical' | 'horizontal';
|
type VideoRatio = 'vertical' | 'horizontal';
|
||||||
|
|
||||||
|
const IMAGES_PER_PAGE = 12;
|
||||||
|
|
||||||
const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
onNext,
|
onNext,
|
||||||
|
onBack,
|
||||||
imageList,
|
imageList,
|
||||||
onRemoveImage,
|
onRemoveImage,
|
||||||
onAddImages,
|
onAddImages,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const imageListRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
const [videoRatio, setVideoRatio] = useState<VideoRatio>('vertical');
|
const [videoRatio, setVideoRatio] = useState<VideoRatio>('vertical');
|
||||||
|
const [displayCount, setDisplayCount] = useState(IMAGES_PER_PAGE);
|
||||||
|
|
||||||
// Load video ratio from localStorage on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedRatio = localStorage.getItem('castad_video_ratio') as VideoRatio;
|
const savedRatio = localStorage.getItem('castad_video_ratio') as VideoRatio;
|
||||||
if (savedRatio === 'vertical' || savedRatio === 'horizontal') {
|
if (savedRatio === 'vertical' || savedRatio === 'horizontal') {
|
||||||
|
|
@ -34,7 +37,6 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Save video ratio to localStorage when it changes
|
|
||||||
const handleVideoRatioChange = (ratio: VideoRatio) => {
|
const handleVideoRatioChange = (ratio: VideoRatio) => {
|
||||||
setVideoRatio(ratio);
|
setVideoRatio(ratio);
|
||||||
localStorage.setItem('castad_video_ratio', ratio);
|
localStorage.setItem('castad_video_ratio', ratio);
|
||||||
|
|
@ -51,7 +53,6 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// URL 이미지와 파일 이미지 분리
|
|
||||||
const urlImages: ImageUrlItem[] = imageList
|
const urlImages: ImageUrlItem[] = imageList
|
||||||
.filter((item: ImageItem): item is ImageItem & { type: 'url' } => item.type === 'url')
|
.filter((item: ImageItem): item is ImageItem & { type: 'url' } => item.type === 'url')
|
||||||
.map((item) => ({ url: item.url }));
|
.map((item) => ({ url: item.url }));
|
||||||
|
|
@ -60,7 +61,6 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
.filter((item: ImageItem): item is ImageItem & { type: 'file' } => item.type === 'file')
|
.filter((item: ImageItem): item is ImageItem & { type: 'file' } => item.type === 'file')
|
||||||
.map((item) => item.file);
|
.map((item) => item.file);
|
||||||
|
|
||||||
// 이미지가 하나라도 있으면 업로드
|
|
||||||
if (urlImages.length > 0 || fileImages.length > 0) {
|
if (urlImages.length > 0 || fileImages.length > 0) {
|
||||||
const response = await uploadImages(urlImages, fileImages);
|
const response = await uploadImages(urlImages, fileImages);
|
||||||
onNext(response.task_id);
|
onNext(response.task_id);
|
||||||
|
|
@ -73,11 +73,6 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageListWheel = (e: React.WheelEvent) => {
|
|
||||||
// 이 영역 안에서는 항상 스크롤 이벤트 전파 차단
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -86,14 +81,10 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
const files = Array.from(e.dataTransfer.files).filter((file: File) =>
|
||||||
const files = Array.from(e.dataTransfer.files).filter(file =>
|
|
||||||
file.type.startsWith('image/')
|
file.type.startsWith('image/')
|
||||||
);
|
);
|
||||||
|
if (files.length > 0) onAddImages(files);
|
||||||
if (files.length > 0) {
|
|
||||||
onAddImages(files);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = () => {
|
const handleFileSelect = () => {
|
||||||
|
|
@ -108,24 +99,48 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const visibleImages = imageList.slice(0, displayCount);
|
||||||
|
const hasMore = imageList.length > displayCount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="page-container">
|
<main className="asset-page">
|
||||||
{/* Title */}
|
{/* Fixed Header - 뒤로가기 버튼 */}
|
||||||
|
<div className="asset-sticky-header">
|
||||||
|
{onBack && (
|
||||||
|
<button onClick={onBack} className="asset-back-btn">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="15 18 9 12 15 6"/>
|
||||||
|
</svg>
|
||||||
|
{t('assetManagement.back')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Single Scrollable Content Area */}
|
||||||
|
<div className="asset-scroll-area">
|
||||||
<h1 className="asset-title">{t('assetManagement.title')}</h1>
|
<h1 className="asset-title">{t('assetManagement.title')}</h1>
|
||||||
|
|
||||||
{/* Main Content Container */}
|
|
||||||
<div className="asset-container">
|
<div className="asset-container">
|
||||||
{/* Left Column - Selected Images */}
|
{/* Left Column - Selected Images */}
|
||||||
<div className="asset-column asset-column-left">
|
<div className="asset-column asset-column-left">
|
||||||
|
<div className="asset-section-header">
|
||||||
|
<div className="asset-section-header-left">
|
||||||
<h3 className="asset-section-title">{t('assetManagement.selectedImages')}</h3>
|
<h3 className="asset-section-title">{t('assetManagement.selectedImages')}</h3>
|
||||||
<div
|
<span className="asset-section-subtitle">{t('assetManagement.minImages')}</span>
|
||||||
ref={imageListRef}
|
</div>
|
||||||
onWheel={handleImageListWheel}
|
<button onClick={handleFileSelect} className="asset-mobile-upload-btn">
|
||||||
className="asset-image-list"
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
>
|
<line x1="8" y1="2" x2="8" y2="14" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||||
{imageList.length > 0 ? (
|
<line x1="2" y1="8" x2="14" y2="8" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
{t('assetManagement.imageUpload')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="asset-image-list">
|
||||||
|
{visibleImages.length > 0 && (
|
||||||
<div className="asset-image-grid">
|
<div className="asset-image-grid">
|
||||||
{imageList.map((item, i) => (
|
{visibleImages.map((item, i) => (
|
||||||
<div key={i} className="asset-image-item">
|
<div key={i} className="asset-image-item">
|
||||||
<img
|
<img
|
||||||
src={getImageSrc(item)}
|
src={getImageSrc(item)}
|
||||||
|
|
@ -146,13 +161,22 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{hasMore && (
|
||||||
|
<button
|
||||||
|
className="asset-load-more"
|
||||||
|
onClick={() => setDisplayCount(prev => prev + IMAGES_PER_PAGE)}
|
||||||
|
>
|
||||||
|
{t('assetManagement.loadMore')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Upload and Video Ratio */}
|
{/* Right Column - Upload and Video Ratio */}
|
||||||
<div className="asset-column asset-column-right">
|
<div className="asset-column asset-column-right">
|
||||||
{/* Image Upload Section */}
|
{/* Image Upload Section (desktop only) */}
|
||||||
<div className="asset-upload-section">
|
<div className="asset-upload-section">
|
||||||
<h3 className="asset-section-title">{t('assetManagement.imageUpload')}</h3>
|
<h3 className="asset-section-title">{t('assetManagement.imageUpload')}</h3>
|
||||||
<div
|
<div
|
||||||
|
|
@ -167,14 +191,6 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
))}
|
))}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
multiple
|
|
||||||
onChange={handleFileChange}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Video Ratio Section */}
|
{/* Video Ratio Section */}
|
||||||
|
|
@ -188,7 +204,8 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
<div className="asset-ratio-icon asset-ratio-icon-vertical">
|
<div className="asset-ratio-icon asset-ratio-icon-vertical">
|
||||||
<div className="asset-ratio-box"></div>
|
<div className="asset-ratio-box"></div>
|
||||||
</div>
|
</div>
|
||||||
<span>9:16</span>
|
<span className="asset-ratio-label">9:16</span>
|
||||||
|
<span className="asset-ratio-subtitle">{t('assetManagement.youtubeShorts')}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleVideoRatioChange('horizontal')}
|
onClick={() => handleVideoRatioChange('horizontal')}
|
||||||
|
|
@ -197,15 +214,17 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
<div className="asset-ratio-icon asset-ratio-icon-horizontal">
|
<div className="asset-ratio-icon asset-ratio-icon-horizontal">
|
||||||
<div className="asset-ratio-box"></div>
|
<div className="asset-ratio-box"></div>
|
||||||
</div>
|
</div>
|
||||||
<span>16:9</span>
|
<span className="asset-ratio-label">16:9</span>
|
||||||
|
<span className="asset-ratio-subtitle">{t('assetManagement.youtubeVideo')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Bottom Button */}
|
{/* Fixed Footer - 다음 단계 버튼 */}
|
||||||
<div className="asset-bottom">
|
<div className="asset-sticky-footer">
|
||||||
{uploadError && (
|
{uploadError && (
|
||||||
<p className="text-red-500 text-sm mb-2">{uploadError}</p>
|
<p className="text-red-500 text-sm mb-2">{uploadError}</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -217,6 +236,15 @@ const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
|
||||||
{isUploading ? t('assetManagement.uploading') : t('assetManagement.nextStep')}
|
{isUploading ? t('assetManagement.uploading') : t('assetManagement.nextStep')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,7 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const [renderProgress, setRenderProgress] = useState(0);
|
const [renderProgress, setRenderProgress] = useState(0);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
|
||||||
const [progress, setProgress] = useState(0);
|
|
||||||
const [showControls, setShowControls] = useState(true);
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
|
||||||
const hasStartedGeneration = useRef(false);
|
const hasStartedGeneration = useRef(false);
|
||||||
const hideControlsTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
// 소셜 미디어 포스팅 모달
|
// 소셜 미디어 포스팅 모달
|
||||||
const [showSocialModal, setShowSocialModal] = useState(false);
|
const [showSocialModal, setShowSocialModal] = useState(false);
|
||||||
|
|
@ -284,62 +279,6 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const togglePlayPause = () => {
|
|
||||||
if (!videoRef.current || !videoUrl) return;
|
|
||||||
if (isPlaying) {
|
|
||||||
videoRef.current.pause();
|
|
||||||
setShowControls(true);
|
|
||||||
if (hideControlsTimer.current) clearTimeout(hideControlsTimer.current);
|
|
||||||
} else {
|
|
||||||
videoRef.current.play();
|
|
||||||
hideControlsTimer.current = setTimeout(() => {
|
|
||||||
setShowControls(false);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
setIsPlaying(!isPlaying);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeUpdate = () => {
|
|
||||||
if (videoRef.current && videoRef.current.duration > 0) {
|
|
||||||
setProgress((videoRef.current.currentTime / videoRef.current.duration) * 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVideoEnded = () => {
|
|
||||||
setIsPlaying(false);
|
|
||||||
setProgress(0);
|
|
||||||
setShowControls(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetHideControlsTimer = () => {
|
|
||||||
if (hideControlsTimer.current) {
|
|
||||||
clearTimeout(hideControlsTimer.current);
|
|
||||||
}
|
|
||||||
setShowControls(true);
|
|
||||||
if (isPlaying) {
|
|
||||||
hideControlsTimer.current = setTimeout(() => {
|
|
||||||
setShowControls(false);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVideoMouseEnter = () => {
|
|
||||||
resetHideControlsTimer();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVideoMouseMove = () => {
|
|
||||||
resetHideControlsTimer();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVideoMouseLeave = () => {
|
|
||||||
if (isPlaying) {
|
|
||||||
if (hideControlsTimer.current) clearTimeout(hideControlsTimer.current);
|
|
||||||
hideControlsTimer.current = setTimeout(() => {
|
|
||||||
setShowControls(false);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
if (videoUrl) {
|
if (videoUrl) {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
|
|
@ -374,7 +313,7 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
// 비디오 해상도 계산
|
// 비디오 해상도 계산
|
||||||
const getVideoResolution = () => {
|
const getVideoResolution = () => {
|
||||||
const savedRatio = localStorage.getItem('castad_video_ratio');
|
const savedRatio = localStorage.getItem('castad_video_ratio');
|
||||||
return savedRatio === 'horizontal' ? '1234×720' : '720×1234';
|
return savedRatio === 'horizontal' ? '1280×720' : '720×1280';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 파일명 생성
|
// 파일명 생성
|
||||||
|
|
@ -403,7 +342,7 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
<div className="comp2-grid">
|
<div className="comp2-grid">
|
||||||
{/* 왼쪽: 영상 */}
|
{/* 왼쪽: 영상 */}
|
||||||
<div className="comp2-video-section">
|
<div className="comp2-video-section">
|
||||||
<div className="comp2-video-wrapper" onMouseEnter={handleVideoMouseEnter} onMouseMove={handleVideoMouseMove} onMouseLeave={handleVideoMouseLeave}>
|
<div className="comp2-video-wrapper">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="comp2-video-loading">
|
<div className="comp2-video-loading">
|
||||||
<div className="loading-spinner">
|
<div className="loading-spinner">
|
||||||
|
|
@ -426,47 +365,12 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : videoUrl ? (
|
) : videoUrl ? (
|
||||||
<>
|
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
|
||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
className="comp2-video-player"
|
className="comp2-video-player"
|
||||||
onTimeUpdate={handleTimeUpdate}
|
controls
|
||||||
onEnded={handleVideoEnded}
|
playsInline
|
||||||
onClick={togglePlayPause}
|
|
||||||
/>
|
/>
|
||||||
<div className={`comp2-video-controls ${showControls ? 'visible' : 'hidden'}`}>
|
|
||||||
<div className="comp2-progress-bar">
|
|
||||||
<div className="comp2-progress-fill" style={{ width: `${progress}%` }}></div>
|
|
||||||
</div>
|
|
||||||
<div className="comp2-controls-row">
|
|
||||||
<button className="comp2-play-btn" onClick={togglePlayPause}>
|
|
||||||
{isPlaying ? (
|
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M8 5v14l11-7z"/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<div className="comp2-volume-control">
|
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
|
||||||
</svg>
|
|
||||||
<div className="comp2-volume-bar">
|
|
||||||
<div className="comp2-volume-fill"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="comp2-time-display">
|
|
||||||
<span className="comp2-time-current">00:04</span>
|
|
||||||
<span className="comp2-time-divider">/</span>
|
|
||||||
<span className="comp2-time-total">00:34</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="comp2-video-placeholder"></div>
|
<div className="comp2-video-placeholder"></div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -363,6 +363,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<AssetManagementContent
|
<AssetManagementContent
|
||||||
|
onBack={() => goToWizardStep(0)}
|
||||||
onNext={(taskId: string) => {
|
onNext={(taskId: string) => {
|
||||||
// Clear video generation state to start fresh
|
// Clear video generation state to start fresh
|
||||||
localStorage.removeItem(VIDEO_GENERATION_KEY);
|
localStorage.removeItem(VIDEO_GENERATION_KEY);
|
||||||
|
|
@ -420,7 +421,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activeItem) {
|
switch (activeItem) {
|
||||||
case '대시보드':
|
case '대시보드':
|
||||||
return <DashboardContent />;
|
return <DashboardContent onNavigate={handleNavigate} />;
|
||||||
case '비즈니스 설정':
|
case '비즈니스 설정':
|
||||||
return <BusinessSettingsContent />;
|
return <BusinessSettingsContent />;
|
||||||
case 'ADO2 콘텐츠':
|
case 'ADO2 콘텐츠':
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
const isGenerating = status === 'generating_lyric' || status === 'generating_song' || status === 'polling';
|
const isGenerating = status === 'generating_lyric' || status === 'generating_song' || status === 'polling';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="page-container">
|
<main className="sound-studio-page">
|
||||||
{audioUrl && (
|
{audioUrl && (
|
||||||
<audio ref={audioRef} src={audioUrl} preload="metadata" />
|
<audio ref={audioRef} src={audioUrl} preload="metadata" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -517,6 +517,41 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Generate Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleGenerateMusic}
|
||||||
|
disabled={isGenerating}
|
||||||
|
className={`btn-generate-sound ${isGenerating ? 'disabled' : ''}`}
|
||||||
|
>
|
||||||
|
{isGenerating ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
||||||
|
</svg>
|
||||||
|
{t('soundStudio.generating')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t('soundStudio.generateButton')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="error-message-new">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isGenerating && statusMessage && (
|
||||||
|
<div className="status-message-new">
|
||||||
|
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
||||||
|
</svg>
|
||||||
|
{statusMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Lyrics */}
|
{/* Right Column - Lyrics */}
|
||||||
|
|
@ -578,41 +613,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Generate Button */}
|
|
||||||
<button
|
|
||||||
onClick={handleGenerateMusic}
|
|
||||||
disabled={isGenerating}
|
|
||||||
className={`btn-generate-sound ${isGenerating ? 'disabled' : ''}`}
|
|
||||||
>
|
|
||||||
{isGenerating ? (
|
|
||||||
<>
|
|
||||||
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
|
||||||
</svg>
|
|
||||||
{t('soundStudio.generating')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t('soundStudio.generateButton')
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{errorMessage && (
|
|
||||||
<div className="error-message-new">
|
|
||||||
{errorMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isGenerating && statusMessage && (
|
|
||||||
<div className="status-message-new">
|
|
||||||
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
|
||||||
</svg>
|
|
||||||
{statusMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Button */}
|
{/* Bottom Button */}
|
||||||
|
|
@ -653,3 +653,4 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SoundStudioContent;
|
export default SoundStudioContent;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
YTAutoSeoResponse,
|
YTAutoSeoResponse,
|
||||||
} from '../types/api';
|
} from '../types/api';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://40.82.133.44';
|
export const API_URL = import.meta.env.VITE_API_URL || 'http://40.82.133.44';
|
||||||
console.log('[API] API_URL:', API_URL);
|
console.log('[API] API_URL:', API_URL);
|
||||||
console.log('[API] VITE_API_URL env:', import.meta.env.VITE_API_URL);
|
console.log('[API] VITE_API_URL env:', import.meta.env.VITE_API_URL);
|
||||||
|
|
||||||
|
|
@ -462,7 +462,7 @@ function redirectToLogin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 401 에러 시 자동으로 토큰 갱신 후 재요청하는 래퍼 함수
|
// 401 에러 시 자동으로 토큰 갱신 후 재요청하는 래퍼 함수
|
||||||
async function authenticatedFetch(
|
export async function authenticatedFetch(
|
||||||
url: string,
|
url: string,
|
||||||
options: RequestInit = {}
|
options: RequestInit = {}
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue