Merge remote-tracking branch 'origin/master' into develop
Showing
48 changed files
with
1297 additions
and
106 deletions
backend/.gitignore
0 → 100644
| ... | @@ -22,7 +22,7 @@ from django.conf import settings | ... | @@ -22,7 +22,7 @@ from django.conf import settings |
| 22 | import jwt | 22 | import jwt |
| 23 | from django.http import HttpResponse, JsonResponse | 23 | from django.http import HttpResponse, JsonResponse |
| 24 | from khudrive.settings import AWS_SESSION_TOKEN, AWS_SECRET_ACCESS_KEY, AWS_ACCESS_KEY_ID, AWS_REGION, \ | 24 | from khudrive.settings import AWS_SESSION_TOKEN, AWS_SECRET_ACCESS_KEY, AWS_ACCESS_KEY_ID, AWS_REGION, \ |
| 25 | - AWS_STORAGE_BUCKET_NAME | 25 | + AWS_STORAGE_BUCKET_NAME, AWS_ENDPOINT_URL |
| 26 | 26 | ||
| 27 | 27 | ||
| 28 | class UserViewSet(viewsets.ModelViewSet): | 28 | class UserViewSet(viewsets.ModelViewSet): |
| ... | @@ -51,6 +51,8 @@ class UserViewSet(viewsets.ModelViewSet): | ... | @@ -51,6 +51,8 @@ class UserViewSet(viewsets.ModelViewSet): |
| 51 | root = Item(is_folder=True, name="root", file_type="folder", path="", user_id=user.int_id, size=0, | 51 | root = Item(is_folder=True, name="root", file_type="folder", path="", user_id=user.int_id, size=0, |
| 52 | status=True) | 52 | status=True) |
| 53 | root.save() | 53 | root.save() |
| 54 | + user.root_folder = root.item_id | ||
| 55 | + user.save() | ||
| 54 | return Response({ | 56 | return Response({ |
| 55 | 'message': 'user created', | 57 | 'message': 'user created', |
| 56 | 'int_id': user.int_id, | 58 | 'int_id': user.int_id, |
| ... | @@ -94,7 +96,15 @@ class UserViewSet(viewsets.ModelViewSet): | ... | @@ -94,7 +96,15 @@ class UserViewSet(viewsets.ModelViewSet): |
| 94 | exp = jwt.decode(access, settings.SECRET_KEY, algorithm='HS256')['exp'] | 96 | exp = jwt.decode(access, settings.SECRET_KEY, algorithm='HS256')['exp'] |
| 95 | token = {'access': access, | 97 | token = {'access': access, |
| 96 | 'refresh': refresh, | 98 | 'refresh': refresh, |
| 97 | - 'exp': exp} | 99 | + 'exp': exp, |
| 100 | + 'user': { | ||
| 101 | + 'int_id': user.int_id, | ||
| 102 | + 'user_id': user.user_id, | ||
| 103 | + 'name': user.name, | ||
| 104 | + 'total_size': user.total_size, | ||
| 105 | + 'current_size': user.current_size, | ||
| 106 | + 'root_folder': user.root_folder | ||
| 107 | + }} | ||
| 98 | return JsonResponse( | 108 | return JsonResponse( |
| 99 | token, | 109 | token, |
| 100 | status=status.HTTP_200_OK, | 110 | status=status.HTTP_200_OK, |
| ... | @@ -173,11 +183,15 @@ class ItemViewSet(viewsets.ViewSet): | ... | @@ -173,11 +183,15 @@ class ItemViewSet(viewsets.ViewSet): |
| 173 | # url: items/11/ | 183 | # url: items/11/ |
| 174 | # 마지막 slash도 써주어야함 | 184 | # 마지막 slash도 써주어야함 |
| 175 | def get(self, request, pk): | 185 | def get(self, request, pk): |
| 176 | - s3 = boto3.client('s3', | 186 | + s3 = boto3.client( |
| 177 | - aws_access_key_id=AWS_ACCESS_KEY_ID, | 187 | + 's3', |
| 178 | - aws_secret_access_key=AWS_SECRET_ACCESS_KEY, | 188 | + region_name=AWS_REGION, |
| 179 | - aws_session_token=AWS_SESSION_TOKEN, | 189 | + aws_access_key_id=AWS_ACCESS_KEY_ID, |
| 180 | - config=Config(signature_version='s3v4')) | 190 | + aws_secret_access_key=AWS_SECRET_ACCESS_KEY, |
| 191 | + aws_session_token=AWS_SESSION_TOKEN, | ||
| 192 | + endpoint_url=AWS_ENDPOINT_URL or None, | ||
| 193 | + config=Config(s3={'addressing_style': 'path'}) | ||
| 194 | + ) | ||
| 181 | s3_bucket = AWS_STORAGE_BUCKET_NAME | 195 | s3_bucket = AWS_STORAGE_BUCKET_NAME |
| 182 | 196 | ||
| 183 | item = Item.objects.filter(item_id=pk) | 197 | item = Item.objects.filter(item_id=pk) |
| ... | @@ -239,29 +253,40 @@ class ItemViewSet(viewsets.ViewSet): | ... | @@ -239,29 +253,40 @@ class ItemViewSet(viewsets.ViewSet): |
| 239 | def move(self, request, pk): | 253 | def move(self, request, pk): |
| 240 | if request.method == 'POST': | 254 | if request.method == 'POST': |
| 241 | parent_id = request.POST.get('parent', '') | 255 | parent_id = request.POST.get('parent', '') |
| 242 | - name = request.POST.get('name', '') | 256 | + name = request.POST.get('name','') |
| 243 | - parent = get_object_or_None(Item, item_id=parent_id) | 257 | + child = get_object_or_None(Item, item_id=pk) |
| 244 | - if parent != None and parent.is_folder == True: | 258 | + |
| 245 | - child = get_object_or_None(Item, item_id=pk) | 259 | + if child == None: |
| 246 | - if child == None: | 260 | + return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT) |
| 247 | - return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT) | 261 | + |
| 248 | - child.parent = parent_id | 262 | + if parent_id != '': |
| 249 | - child.save() | 263 | + parent = get_object_or_None(Item, item_id=parent_id) |
| 250 | - child = Item.objects.filter(item_id=pk) | 264 | + |
| 251 | - child_data = serializers.serialize("json", child) | 265 | + if parent == None: |
| 252 | - json_child = json.loads(child_data) | 266 | + return Response({'message': 'parent is not existed.'}, status=status.HTTP_200_OK) |
| 253 | - res = json_child[0]['fields'] | 267 | + if parent.is_folder == False: |
| 254 | - res['id'] = pk | 268 | + return Response({'message': 'parent is not folder.'}, status=status.HTTP_200_OK) |
| 255 | - parent = Item.objects.filter(item_id=parent_id) | 269 | + |
| 256 | - parent_data = serializers.serialize("json", parent) | 270 | + if parent != None and parent.is_folder == True: |
| 257 | - json_parent = json.loads(parent_data)[0]['fields'] | 271 | + child.parent = parent_id |
| 258 | - res['parentInfo'] = json_parent | 272 | + else: |
| 259 | - return Response({'data': res}, status=status.HTTP_200_OK) | 273 | + parent_id = child.parent |
| 260 | - if parent == None: | 274 | + |
| 261 | - return Response({'message': 'parent is not existed.'}, status=status.HTTP_200_OK) | 275 | + if name != '': |
| 262 | - if parent.is_folder == False: | 276 | + child.name = name; |
| 263 | - return Response({'message': 'parent is not folder.'}, status=status.HTTP_200_OK) | 277 | + |
| 264 | - return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT) | 278 | + child.save() |
| 279 | + child = Item.objects.filter(item_id = pk) | ||
| 280 | + child_data = serializers.serialize("json", child) | ||
| 281 | + json_child = json.loads(child_data) | ||
| 282 | + res = json_child[0]['fields'] | ||
| 283 | + res['id'] = pk | ||
| 284 | + parent = Item.objects.filter(item_id = parent_id) | ||
| 285 | + parent_data = serializers.serialize("json", parent) | ||
| 286 | + json_parent = json.loads(parent_data)[0]['fields'] | ||
| 287 | + res['parentInfo'] = json_parent | ||
| 288 | + | ||
| 289 | + return Response({'data': res}, status=status.HTTP_200_OK) | ||
| 265 | 290 | ||
| 266 | @action(methods=['POST'], detail=True, permission_classes=[AllowAny], url_path='copy', url_name='copy') | 291 | @action(methods=['POST'], detail=True, permission_classes=[AllowAny], url_path='copy', url_name='copy') |
| 267 | def copy(self, request, pk): | 292 | def copy(self, request, pk): |
| ... | @@ -309,7 +334,7 @@ class ItemViewSet(viewsets.ViewSet): | ... | @@ -309,7 +334,7 @@ class ItemViewSet(viewsets.ViewSet): |
| 309 | url_path='children', url_name='children') | 334 | url_path='children', url_name='children') |
| 310 | def children(self, request, pk): | 335 | def children(self, request, pk): |
| 311 | if request.method == 'GET': | 336 | if request.method == 'GET': |
| 312 | - children = Item.objects.filter(parent=pk, is_deleted=False) | 337 | + children = Item.objects.filter(parent=pk, is_deleted=False, status=True) |
| 313 | children_data = serializers.serialize("json", children) | 338 | children_data = serializers.serialize("json", children) |
| 314 | json_children = json.loads(children_data) | 339 | json_children = json.loads(children_data) |
| 315 | parent = Item.objects.filter(item_id=pk) # item | 340 | parent = Item.objects.filter(item_id=pk) # item |
| ... | @@ -360,7 +385,15 @@ class ItemViewSet(viewsets.ViewSet): | ... | @@ -360,7 +385,15 @@ class ItemViewSet(viewsets.ViewSet): |
| 360 | url_path='upload', url_name='upload') | 385 | url_path='upload', url_name='upload') |
| 361 | def upload(self, request, pk): | 386 | def upload(self, request, pk): |
| 362 | if request.method == 'POST': | 387 | if request.method == 'POST': |
| 363 | - s3 = boto3.client('s3') | 388 | + s3 = boto3.client( |
| 389 | + 's3', | ||
| 390 | + region_name=AWS_REGION, | ||
| 391 | + aws_access_key_id=AWS_ACCESS_KEY_ID, | ||
| 392 | + aws_secret_access_key=AWS_SECRET_ACCESS_KEY, | ||
| 393 | + aws_session_token=AWS_SESSION_TOKEN, | ||
| 394 | + endpoint_url=AWS_ENDPOINT_URL or None, | ||
| 395 | + config=Config(s3={'addressing_style': 'path'}) | ||
| 396 | + ) | ||
| 364 | s3_bucket = AWS_STORAGE_BUCKET_NAME | 397 | s3_bucket = AWS_STORAGE_BUCKET_NAME |
| 365 | 398 | ||
| 366 | # 파일 객체 생성 | 399 | # 파일 객체 생성 |
| ... | @@ -379,6 +412,7 @@ class ItemViewSet(viewsets.ViewSet): | ... | @@ -379,6 +412,7 @@ class ItemViewSet(viewsets.ViewSet): |
| 379 | { | 412 | { |
| 380 | "acl": "private", | 413 | "acl": "private", |
| 381 | "Content-Type": file_type, | 414 | "Content-Type": file_type, |
| 415 | + "Content-Disposition": "attachment", | ||
| 382 | 'region': AWS_REGION, | 416 | 'region': AWS_REGION, |
| 383 | 'x-amz-algorithm': 'AWS4-HMAC-SHA256', | 417 | 'x-amz-algorithm': 'AWS4-HMAC-SHA256', |
| 384 | 'x-amz-date': date_long | 418 | 'x-amz-date': date_long |
| ... | @@ -386,18 +420,26 @@ class ItemViewSet(viewsets.ViewSet): | ... | @@ -386,18 +420,26 @@ class ItemViewSet(viewsets.ViewSet): |
| 386 | [ | 420 | [ |
| 387 | {"acl": "private"}, | 421 | {"acl": "private"}, |
| 388 | {"Content-Type": file_type}, | 422 | {"Content-Type": file_type}, |
| 423 | + {"Content-Disposition": "attachment"}, | ||
| 389 | {'x-amz-algorithm': 'AWS4-HMAC-SHA256'}, | 424 | {'x-amz-algorithm': 'AWS4-HMAC-SHA256'}, |
| 390 | {'x-amz-date': date_long} | 425 | {'x-amz-date': date_long} |
| 391 | ], | 426 | ], |
| 392 | 3600 | 427 | 3600 |
| 393 | ) | 428 | ) |
| 394 | 429 | ||
| 430 | + item = Item.objects.filter(item_id=upload_item.item_id) | ||
| 431 | + item_data = serializers.serialize("json", item) | ||
| 432 | + json_item = json.loads(item_data) | ||
| 433 | + res = json_item[0]['fields'] | ||
| 434 | + res['id'] = json_item[0]['pk'] | ||
| 435 | + | ||
| 395 | data = { | 436 | data = { |
| 396 | "signed_url": presigned_post, | 437 | "signed_url": presigned_post, |
| 397 | - 'url': 'https://%s.s3.amazonaws.com/%s' % (s3_bucket, file_name) | 438 | + 'url': '%s/%s' % (presigned_post["url"], file_name), |
| 439 | + 'item': res | ||
| 398 | } | 440 | } |
| 399 | 441 | ||
| 400 | - return Response({'presigned_post': presigned_post, 'proc_data': data}, status=status.HTTP_200_OK) | 442 | + return Response(data, status=status.HTTP_200_OK) |
| 401 | 443 | ||
| 402 | # url: /status/ | 444 | # url: /status/ |
| 403 | @action(methods=['POST'], detail=True, permission_classes=[AllowAny], | 445 | @action(methods=['POST'], detail=True, permission_classes=[AllowAny], | ... | ... |
backend/docker-compose.yml
0 → 100644
| 1 | +version: "3" | ||
| 2 | +services: | ||
| 3 | + postgres: | ||
| 4 | + image: "postgres:alpine" | ||
| 5 | + environment: | ||
| 6 | + - POSTGRES_USER=khudrive | ||
| 7 | + - POSTGRES_PASSWORD=4REPwb7y4CLtQaTv4PNeWRJeGLbHXn | ||
| 8 | + - POSTGRES_DB=khudrive | ||
| 9 | + ports: | ||
| 10 | + - "35432:5432" | ||
| 11 | + volumes: | ||
| 12 | + - ./docker/postgres:/var/lib/postgresql/data/ | ||
| 13 | + minio: | ||
| 14 | + image: "minio/minio" | ||
| 15 | + entrypoint: sh | ||
| 16 | + command: -c "mkdir -p /data/bucket && /usr/bin/minio server /data" | ||
| 17 | + environment: | ||
| 18 | + - MINIO_ACCESS_KEY=access_key | ||
| 19 | + - MINIO_SECRET_KEY=secret_key | ||
| 20 | + ports: | ||
| 21 | + - "39000:9000" | ||
| 22 | + volumes: | ||
| 23 | + - ./docker/minio:/data | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -88,11 +88,11 @@ DATABASES = { | ... | @@ -88,11 +88,11 @@ DATABASES = { |
| 88 | # } | 88 | # } |
| 89 | 'default': { | 89 | 'default': { |
| 90 | 'ENGINE': 'django.db.backends.postgresql', | 90 | 'ENGINE': 'django.db.backends.postgresql', |
| 91 | - 'NAME': 'drive', | 91 | + 'NAME': 'khudrive', |
| 92 | - 'USER': 'jooheekwon', | 92 | + 'USER': 'khudrive', |
| 93 | - 'PASSWORD': 'victoriawngml77', | 93 | + 'PASSWORD': '4REPwb7y4CLtQaTv4PNeWRJeGLbHXn', |
| 94 | 'HOST': 'localhost', | 94 | 'HOST': 'localhost', |
| 95 | - 'PORT': '', | 95 | + 'PORT': '35432', |
| 96 | } | 96 | } |
| 97 | } | 97 | } |
| 98 | 98 | ... | ... |
| 1 | asgiref==3.2.7 | 1 | asgiref==3.2.7 |
| 2 | +boto3==1.14.2 | ||
| 3 | +botocore==1.17.2 | ||
| 4 | +cffi==1.14.0 | ||
| 5 | +cryptography==2.9.2 | ||
| 2 | Django==3.0.6 | 6 | Django==3.0.6 |
| 7 | +django-annoying==0.10.6 | ||
| 3 | djangorestframework==3.11.0 | 8 | djangorestframework==3.11.0 |
| 9 | +docutils==0.15.2 | ||
| 10 | +jmespath==0.10.0 | ||
| 11 | +psycopg2==2.8.5 | ||
| 12 | +pycparser==2.20 | ||
| 13 | +PyJWT==1.7.1 | ||
| 14 | +python-dateutil==2.8.1 | ||
| 4 | pytz==2020.1 | 15 | pytz==2020.1 |
| 16 | +s3transfer==0.3.3 | ||
| 17 | +six==1.15.0 | ||
| 5 | sqlparse==0.3.1 | 18 | sqlparse==0.3.1 |
| 19 | +urllib3==1.25.9 | ... | ... |
| ... | @@ -21,6 +21,7 @@ | ... | @@ -21,6 +21,7 @@ |
| 21 | "react/display-name": "off", | 21 | "react/display-name": "off", |
| 22 | "react/prop-types": "off", | 22 | "react/prop-types": "off", |
| 23 | "no-empty": ["warn", { "allowEmptyCatch": true }], | 23 | "no-empty": ["warn", { "allowEmptyCatch": true }], |
| 24 | + "@typescript-eslint/camelcase": "off", | ||
| 24 | "@typescript-eslint/explicit-function-return-type": "off", | 25 | "@typescript-eslint/explicit-function-return-type": "off", |
| 25 | "@typescript-eslint/explicit-member-accessibility": "off", | 26 | "@typescript-eslint/explicit-member-accessibility": "off", |
| 26 | "@typescript-eslint/interface-name-prefix": "off", | 27 | "@typescript-eslint/interface-name-prefix": "off", | ... | ... |
| 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
| 2 | 2 | ||
| 3 | # dependencies | 3 | # dependencies |
| 4 | -/backend/env | 4 | +/node_modules |
| 5 | -/frontend/node_modules | 5 | +/.pnp |
| 6 | -/frontend/.pnp | ||
| 7 | .pnp.js | 6 | .pnp.js |
| 8 | 7 | ||
| 9 | # testing | 8 | # testing |
| 10 | -/frontend/coverage | 9 | +/coverage |
| 11 | 10 | ||
| 12 | # production | 11 | # production |
| 13 | -/frontend/build | 12 | +/build |
| 14 | - | ||
| 15 | -# database | ||
| 16 | -/backend/db.sqlite3 | ||
| 17 | 13 | ||
| 18 | # misc | 14 | # misc |
| 19 | .DS_Store | 15 | .DS_Store |
| ... | @@ -21,7 +17,6 @@ | ... | @@ -21,7 +17,6 @@ |
| 21 | .env.development.local | 17 | .env.development.local |
| 22 | .env.test.local | 18 | .env.test.local |
| 23 | .env.production.local | 19 | .env.production.local |
| 24 | -__pycache__ | ||
| 25 | 20 | ||
| 26 | npm-debug.log* | 21 | npm-debug.log* |
| 27 | yarn-debug.log* | 22 | yarn-debug.log* | ... | ... |
This diff is collapsed. Click to expand it.
| ... | @@ -4,37 +4,41 @@ | ... | @@ -4,37 +4,41 @@ |
| 4 | "description": "Dropbox alternative cloud file service", | 4 | "description": "Dropbox alternative cloud file service", |
| 5 | "private": true, | 5 | "private": true, |
| 6 | "dependencies": { | 6 | "dependencies": { |
| 7 | + "@ant-design/icons": "^4.2.1", | ||
| 8 | + "antd": "^4.3.3", | ||
| 7 | "classnames": "^2.2.6", | 9 | "classnames": "^2.2.6", |
| 8 | - "ky": "^0.19.1", | 10 | + "filesize": "^6.1.0", |
| 11 | + "ky": "^0.20.0", | ||
| 12 | + "miragejs": "^0.1.40", | ||
| 9 | "react": "^16.13.1", | 13 | "react": "^16.13.1", |
| 10 | "react-dom": "^16.13.1", | 14 | "react-dom": "^16.13.1", |
| 11 | - "react-router-dom": "^5.1.2" | 15 | + "react-router-dom": "^5.2.0" |
| 12 | }, | 16 | }, |
| 13 | "devDependencies": { | 17 | "devDependencies": { |
| 14 | "@hot-loader/react-dom": "^16.13.0", | 18 | "@hot-loader/react-dom": "^16.13.0", |
| 15 | - "@testing-library/jest-dom": "^5.7.0", | 19 | + "@testing-library/jest-dom": "^5.9.0", |
| 16 | - "@testing-library/react": "^10.0.4", | 20 | + "@testing-library/react": "^10.2.0", |
| 17 | - "@testing-library/user-event": "^10.1.2", | 21 | + "@testing-library/user-event": "^11.2.0", |
| 18 | "@types/classnames": "^2.2.10", | 22 | "@types/classnames": "^2.2.10", |
| 19 | - "@types/jest": "^25.2.1", | 23 | + "@types/jest": "^25.2.3", |
| 20 | "@types/node": "12", | 24 | "@types/node": "12", |
| 21 | "@types/react": "^16.9.35", | 25 | "@types/react": "^16.9.35", |
| 22 | "@types/react-dom": "^16.9.8", | 26 | "@types/react-dom": "^16.9.8", |
| 23 | "@types/react-router-dom": "^5.1.5", | 27 | "@types/react-router-dom": "^5.1.5", |
| 24 | - "@typescript-eslint/eslint-plugin": "^2.31.0", | 28 | + "@typescript-eslint/eslint-plugin": "^2.34.0", |
| 25 | - "@typescript-eslint/parser": "^2.31.0", | 29 | + "@typescript-eslint/parser": "^2.34.0", |
| 26 | - "customize-cra": "0.9.1", | 30 | + "customize-cra": "1.0.0", |
| 27 | "eslint-config-prettier": "^6.11.0", | 31 | "eslint-config-prettier": "^6.11.0", |
| 28 | - "eslint-plugin-jest": "^23.10.0", | 32 | + "eslint-plugin-jest": "^23.13.2", |
| 29 | "husky": "^4.2.5", | 33 | "husky": "^4.2.5", |
| 30 | - "lint-staged": "^10.2.2", | 34 | + "lint-staged": "^10.2.9", |
| 31 | "node-sass": "^4.14.1", | 35 | "node-sass": "^4.14.1", |
| 32 | "prettier": "^2.0.5", | 36 | "prettier": "^2.0.5", |
| 33 | "react-app-rewired": "^2.1.6", | 37 | "react-app-rewired": "^2.1.6", |
| 34 | "react-hot-loader": "^4.12.21", | 38 | "react-hot-loader": "^4.12.21", |
| 35 | "react-scripts": "3.4.1", | 39 | "react-scripts": "3.4.1", |
| 36 | - "typescript": "^3.8.3", | 40 | + "typescript": "^3.9.5", |
| 37 | - "webpack-bundle-analyzer": "^3.7.0" | 41 | + "webpack-bundle-analyzer": "^3.8.0" |
| 38 | }, | 42 | }, |
| 39 | "scripts": { | 43 | "scripts": { |
| 40 | "start": "react-app-rewired start", | 44 | "start": "react-app-rewired start", |
| ... | @@ -63,5 +67,6 @@ | ... | @@ -63,5 +67,6 @@ |
| 63 | }, | 67 | }, |
| 64 | "lint-staged": { | 68 | "lint-staged": { |
| 65 | "*.{js,ts,tsx,json,css,scss}": "prettier --write" | 69 | "*.{js,ts,tsx,json,css,scss}": "prettier --write" |
| 66 | - } | 70 | + }, |
| 71 | + "proxy": "http://localhost:8000" | ||
| 67 | } | 72 | } | ... | ... |
frontend/public/android-chrome-192x192.png
0 → 100644
1.67 KB
frontend/public/android-chrome-512x512.png
0 → 100644
5.26 KB
frontend/public/apple-touch-icon.png
0 → 100644
1.53 KB
frontend/public/browserconfig.xml
0 → 100644
frontend/public/favicon-16x16.png
0 → 100644
605 Bytes
frontend/public/favicon-32x32.png
0 → 100644
699 Bytes
No preview for this file type
| ... | @@ -2,42 +2,21 @@ | ... | @@ -2,42 +2,21 @@ |
| 2 | <html lang="en"> | 2 | <html lang="en"> |
| 3 | <head> | 3 | <head> |
| 4 | <meta charset="utf-8" /> | 4 | <meta charset="utf-8" /> |
| 5 | - <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | ||
| 6 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| 7 | <meta name="theme-color" content="#000000" /> | 6 | <meta name="theme-color" content="#000000" /> |
| 8 | - <meta | 7 | + <meta name="description" content="KHUDrive" /> |
| 9 | - name="description" | 8 | + <link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png"> |
| 10 | - content="Web site created using create-react-app" | 9 | + <link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png"> |
| 11 | - /> | 10 | + <link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png"> |
| 12 | - <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> | 11 | + <link rel="manifest" href="%PUBLIC_URL%/site.webmanifest"> |
| 13 | - <!-- | 12 | + <link rel="mask-icon" href="%PUBLIC_URL%/safari-pinned-tab.svg" color="#5bbad5"> |
| 14 | - manifest.json provides metadata used when your web app is installed on a | 13 | + <meta name="msapplication-TileColor" content="#da532c"> |
| 15 | - user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ | 14 | + <meta name="theme-color" content="#ffffff"> |
| 16 | - --> | ||
| 17 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> | 15 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
| 18 | - <!-- | 16 | + <title>KHUDrive</title> |
| 19 | - Notice the use of %PUBLIC_URL% in the tags above. | ||
| 20 | - It will be replaced with the URL of the `public` folder during the build. | ||
| 21 | - Only files inside the `public` folder can be referenced from the HTML. | ||
| 22 | - | ||
| 23 | - Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will | ||
| 24 | - work correctly both with client-side routing and a non-root public URL. | ||
| 25 | - Learn how to configure a non-root public URL by running `npm run build`. | ||
| 26 | - --> | ||
| 27 | - <title>React App</title> | ||
| 28 | </head> | 17 | </head> |
| 29 | <body> | 18 | <body> |
| 30 | <noscript>You need to enable JavaScript to run this app.</noscript> | 19 | <noscript>You need to enable JavaScript to run this app.</noscript> |
| 31 | <div id="root"></div> | 20 | <div id="root"></div> |
| 32 | - <!-- | ||
| 33 | - This HTML file is a template. | ||
| 34 | - If you open it directly in the browser, you will see an empty page. | ||
| 35 | - | ||
| 36 | - You can add webfonts, meta tags, or analytics to this file. | ||
| 37 | - The build step will place the bundled scripts into the <body> tag. | ||
| 38 | - | ||
| 39 | - To begin the development, run `npm start` or `yarn start`. | ||
| 40 | - To create a production bundle, use `npm run build` or `yarn build`. | ||
| 41 | - --> | ||
| 42 | </body> | 21 | </body> |
| 43 | </html> | 22 | </html> | ... | ... |
frontend/public/mstile-144x144.png
0 → 100644
1.3 KB
frontend/public/mstile-150x150.png
0 → 100644
1.33 KB
frontend/public/mstile-310x150.png
0 → 100644
1.49 KB
frontend/public/mstile-310x310.png
0 → 100644
2.74 KB
frontend/public/mstile-70x70.png
0 → 100644
984 Bytes
frontend/public/safari-pinned-tab.svg
0 → 100644
| 1 | +<?xml version="1.0" standalone="no"?> | ||
| 2 | +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" | ||
| 3 | + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> | ||
| 4 | +<svg version="1.0" xmlns="http://www.w3.org/2000/svg" | ||
| 5 | + width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000" | ||
| 6 | + preserveAspectRatio="xMidYMid meet"> | ||
| 7 | +<metadata> | ||
| 8 | +Created by potrace 1.11, written by Peter Selinger 2001-2013 | ||
| 9 | +</metadata> | ||
| 10 | +<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)" | ||
| 11 | +fill="#000000" stroke="none"> | ||
| 12 | +<path d="M300 6180 c-105 -20 -218 -112 -268 -218 l-27 -57 0 -2410 0 -2410 | ||
| 13 | +29 -58 c50 -98 123 -163 223 -196 l63 -21 3197 2 3198 3 65 31 c80 38 149 105 | ||
| 14 | +187 182 l28 57 0 2040 0 2040 -33 67 c-48 99 -134 169 -242 198 -34 9 -494 12 | ||
| 15 | +-1930 14 l-1885 1 -65 31 c-96 46 -150 109 -240 279 -70 132 -165 287 -194 | ||
| 16 | +317 -46 47 -112 86 -175 103 -38 10 -247 13 -972 12 -508 0 -940 -3 -959 -7z"/> | ||
| 17 | +</g> | ||
| 18 | +</svg> |
frontend/public/site.webmanifest
0 → 100644
| 1 | +{ | ||
| 2 | + "name": "", | ||
| 3 | + "short_name": "", | ||
| 4 | + "icons": [ | ||
| 5 | + { | ||
| 6 | + "src": "/android-chrome-192x192.png", | ||
| 7 | + "sizes": "192x192", | ||
| 8 | + "type": "image/png" | ||
| 9 | + }, | ||
| 10 | + { | ||
| 11 | + "src": "/android-chrome-512x512.png", | ||
| 12 | + "sizes": "512x512", | ||
| 13 | + "type": "image/png" | ||
| 14 | + } | ||
| 15 | + ], | ||
| 16 | + "theme_color": "#ffffff", | ||
| 17 | + "background_color": "#ffffff", | ||
| 18 | + "display": "standalone" | ||
| 19 | +} |
| 1 | import React from "react"; | 1 | import React from "react"; |
| 2 | +import { Switch, Route, Redirect } from "react-router-dom"; | ||
| 3 | + | ||
| 4 | +import { Login } from "auth/Login"; | ||
| 5 | +import { Signup } from "auth/Signup"; | ||
| 6 | +import { useAuth, TokenContext } from "auth/useAuth"; | ||
| 7 | + | ||
| 8 | +import { Page } from "layout/Page"; | ||
| 9 | +import { FileList } from "file/FileList"; | ||
| 2 | 10 | ||
| 3 | export function App() { | 11 | export function App() { |
| 4 | - return <div>Hello World!</div>; | 12 | + const token = useAuth(); |
| 13 | + const root = token?.token?.user.rootFolder; | ||
| 14 | + | ||
| 15 | + return ( | ||
| 16 | + <Switch> | ||
| 17 | + <Route path="/login"> | ||
| 18 | + <Login login={token.login} /> | ||
| 19 | + </Route> | ||
| 20 | + <Route path="/signup"> | ||
| 21 | + <Signup /> | ||
| 22 | + </Route> | ||
| 23 | + <Route> | ||
| 24 | + {token.token !== null ? ( | ||
| 25 | + <TokenContext.Provider value={token}> | ||
| 26 | + <Page> | ||
| 27 | + <Switch> | ||
| 28 | + <Route path="/folder/:id"> | ||
| 29 | + <FileList /> | ||
| 30 | + </Route> | ||
| 31 | + <Route> | ||
| 32 | + <Redirect to={`/folder/${root}`} /> | ||
| 33 | + </Route> | ||
| 34 | + </Switch> | ||
| 35 | + </Page> | ||
| 36 | + </TokenContext.Provider> | ||
| 37 | + ) : ( | ||
| 38 | + <Redirect to="/login" /> | ||
| 39 | + )} | ||
| 40 | + </Route> | ||
| 41 | + </Switch> | ||
| 42 | + ); | ||
| 5 | } | 43 | } | ... | ... |
frontend/src/auth/Login.module.scss
0 → 100644
| 1 | +.layout { | ||
| 2 | + height: 100%; | ||
| 3 | + align-items: center; | ||
| 4 | + justify-content: center; | ||
| 5 | +} | ||
| 6 | + | ||
| 7 | +.content { | ||
| 8 | + width: 640px; | ||
| 9 | + flex-grow: 0; | ||
| 10 | + background: #fff; | ||
| 11 | + padding: 80px 50px 50px; | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +#components-form-demo-normal-login .login-form-forgot { | ||
| 15 | + float: right; | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +#components-form-demo-normal-login .ant-col-rtl .login-form-forgot { | ||
| 19 | + float: left; | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +#components-form-demo-normal-login .login-form-button { | ||
| 23 | + width: 100%; | ||
| 24 | +} |
frontend/src/auth/Login.tsx
0 → 100644
| 1 | +import React, { useCallback, useState } from "react"; | ||
| 2 | +import { Form, Input, Button, Checkbox, Layout } from "antd"; | ||
| 3 | +import { UserOutlined, LockOutlined } from "@ant-design/icons"; | ||
| 4 | +import { useHistory, Link } from "react-router-dom"; | ||
| 5 | + | ||
| 6 | +import styles from "./Login.module.scss"; | ||
| 7 | + | ||
| 8 | +export type LoginProps = { | ||
| 9 | + login: ( | ||
| 10 | + username: string, | ||
| 11 | + password: string, | ||
| 12 | + remember: boolean | ||
| 13 | + ) => Promise<void>; | ||
| 14 | +}; | ||
| 15 | + | ||
| 16 | +export function Login({ login }: LoginProps) { | ||
| 17 | + const [error, setError] = useState<boolean>(false); | ||
| 18 | + const history = useHistory(); | ||
| 19 | + | ||
| 20 | + const handleLogin = useCallback( | ||
| 21 | + async ({ username, password, remember }) => { | ||
| 22 | + setError(false); | ||
| 23 | + try { | ||
| 24 | + await login(username, password, remember); | ||
| 25 | + history.push("/"); | ||
| 26 | + } catch { | ||
| 27 | + setError(true); | ||
| 28 | + } | ||
| 29 | + }, | ||
| 30 | + [login, history] | ||
| 31 | + ); | ||
| 32 | + | ||
| 33 | + return ( | ||
| 34 | + <Layout className={styles.layout}> | ||
| 35 | + <Layout.Content className={styles.content}> | ||
| 36 | + <Form | ||
| 37 | + name="login" | ||
| 38 | + initialValues={{ remember: true }} | ||
| 39 | + onFinish={handleLogin} | ||
| 40 | + > | ||
| 41 | + <Form.Item | ||
| 42 | + name="username" | ||
| 43 | + rules={[{ required: true, message: "아이디를 입력하세요" }]} | ||
| 44 | + {...(error && { | ||
| 45 | + validateStatus: "error", | ||
| 46 | + })} | ||
| 47 | + > | ||
| 48 | + <Input prefix={<UserOutlined />} placeholder="아이디" /> | ||
| 49 | + </Form.Item> | ||
| 50 | + <Form.Item | ||
| 51 | + name="password" | ||
| 52 | + rules={[{ required: true, message: "비밀번호를 입력하세요" }]} | ||
| 53 | + {...(error && { | ||
| 54 | + validateStatus: "error", | ||
| 55 | + help: "로그인에 실패했습니다", | ||
| 56 | + })} | ||
| 57 | + > | ||
| 58 | + <Input | ||
| 59 | + prefix={<LockOutlined />} | ||
| 60 | + type="password" | ||
| 61 | + placeholder="비밀번호" | ||
| 62 | + /> | ||
| 63 | + </Form.Item> | ||
| 64 | + <Form.Item> | ||
| 65 | + <Form.Item name="remember" valuePropName="checked" noStyle> | ||
| 66 | + <Checkbox>자동 로그인</Checkbox> | ||
| 67 | + </Form.Item> | ||
| 68 | + </Form.Item> | ||
| 69 | + | ||
| 70 | + <Form.Item> | ||
| 71 | + <Button type="primary" htmlType="submit"> | ||
| 72 | + 로그인 | ||
| 73 | + </Button> | ||
| 74 | + <Link to="/signup" style={{ marginLeft: 24 }}> | ||
| 75 | + 회원가입 | ||
| 76 | + </Link> | ||
| 77 | + </Form.Item> | ||
| 78 | + </Form> | ||
| 79 | + </Layout.Content> | ||
| 80 | + </Layout> | ||
| 81 | + ); | ||
| 82 | +} |
frontend/src/auth/Signup.tsx
0 → 100644
| 1 | +import React, { useCallback, useState } from "react"; | ||
| 2 | +import { Form, Input, Button, Layout, message } from "antd"; | ||
| 3 | +import { UserOutlined, LockOutlined, TagOutlined } from "@ant-design/icons"; | ||
| 4 | +import { useHistory } from "react-router-dom"; | ||
| 5 | + | ||
| 6 | +import styles from "./Login.module.scss"; | ||
| 7 | +import ky from "ky"; | ||
| 8 | + | ||
| 9 | +export function Signup() { | ||
| 10 | + const [error, setError] = useState<boolean>(false); | ||
| 11 | + const [check, setCheck] = useState<boolean>(false); | ||
| 12 | + const history = useHistory(); | ||
| 13 | + | ||
| 14 | + const handleSignup = useCallback( | ||
| 15 | + async ({ user_id, password, password_check, name }) => { | ||
| 16 | + if (password !== password_check) { | ||
| 17 | + return setCheck(true); | ||
| 18 | + } else { | ||
| 19 | + setCheck(false); | ||
| 20 | + } | ||
| 21 | + | ||
| 22 | + setError(false); | ||
| 23 | + try { | ||
| 24 | + const body = new URLSearchParams(); | ||
| 25 | + body.set("user_id", user_id); | ||
| 26 | + body.set("password", password); | ||
| 27 | + body.set("name", name); | ||
| 28 | + | ||
| 29 | + await ky.post("/users/signup/", { body }); | ||
| 30 | + | ||
| 31 | + message.success("회원가입이 완료되었습니다"); | ||
| 32 | + history.push("/login"); | ||
| 33 | + } catch { | ||
| 34 | + setError(true); | ||
| 35 | + } | ||
| 36 | + }, | ||
| 37 | + [history] | ||
| 38 | + ); | ||
| 39 | + | ||
| 40 | + return ( | ||
| 41 | + <Layout className={styles.layout}> | ||
| 42 | + <Layout.Content className={styles.content}> | ||
| 43 | + <Form name="signup" onFinish={handleSignup}> | ||
| 44 | + <Form.Item | ||
| 45 | + name="user_id" | ||
| 46 | + rules={[{ required: true, message: "아이디를 입력하세요" }]} | ||
| 47 | + {...(error && { | ||
| 48 | + validateStatus: "error", | ||
| 49 | + })} | ||
| 50 | + > | ||
| 51 | + <Input prefix={<UserOutlined />} placeholder="아이디" /> | ||
| 52 | + </Form.Item> | ||
| 53 | + <Form.Item | ||
| 54 | + name="password" | ||
| 55 | + rules={[{ required: true, message: "비밀번호를 입력하세요" }]} | ||
| 56 | + {...(error && { | ||
| 57 | + validateStatus: "error", | ||
| 58 | + help: "로그인에 실패했습니다", | ||
| 59 | + })} | ||
| 60 | + > | ||
| 61 | + <Input | ||
| 62 | + prefix={<LockOutlined />} | ||
| 63 | + type="password" | ||
| 64 | + placeholder="비밀번호" | ||
| 65 | + /> | ||
| 66 | + </Form.Item> | ||
| 67 | + <Form.Item | ||
| 68 | + name="password_check" | ||
| 69 | + rules={[ | ||
| 70 | + { required: true, message: "비밀번호를 한번 더 입력하세요" }, | ||
| 71 | + ]} | ||
| 72 | + {...(error && { | ||
| 73 | + validateStatus: "error", | ||
| 74 | + help: "로그인에 실패했습니다", | ||
| 75 | + })} | ||
| 76 | + {...(check && { | ||
| 77 | + validateStatus: "error", | ||
| 78 | + help: "비밀번호가 일치하지 않습니다", | ||
| 79 | + })} | ||
| 80 | + > | ||
| 81 | + <Input | ||
| 82 | + prefix={<LockOutlined />} | ||
| 83 | + type="password" | ||
| 84 | + placeholder="비밀번호 확인" | ||
| 85 | + /> | ||
| 86 | + </Form.Item> | ||
| 87 | + <Form.Item | ||
| 88 | + name="name" | ||
| 89 | + rules={[{ required: true, message: "이름을 입력하세요" }]} | ||
| 90 | + {...(error && { | ||
| 91 | + validateStatus: "error", | ||
| 92 | + })} | ||
| 93 | + > | ||
| 94 | + <Input prefix={<TagOutlined />} placeholder="이름" /> | ||
| 95 | + </Form.Item> | ||
| 96 | + | ||
| 97 | + <Form.Item> | ||
| 98 | + <Button type="primary" htmlType="submit"> | ||
| 99 | + 회원 가입 | ||
| 100 | + </Button> | ||
| 101 | + </Form.Item> | ||
| 102 | + </Form> | ||
| 103 | + </Layout.Content> | ||
| 104 | + </Layout> | ||
| 105 | + ); | ||
| 106 | +} |
frontend/src/auth/useAuth.ts
0 → 100644
| 1 | +import React, { useState, useCallback } from "react"; | ||
| 2 | +import ky from "ky"; | ||
| 3 | + | ||
| 4 | +interface LoginResponse { | ||
| 5 | + access: string; | ||
| 6 | + refresh: string; | ||
| 7 | + exp: number; | ||
| 8 | + user: { | ||
| 9 | + int_id: number; | ||
| 10 | + user_id: string; | ||
| 11 | + name: string; | ||
| 12 | + total_size: number; | ||
| 13 | + current_size: number; | ||
| 14 | + root_folder: number; | ||
| 15 | + }; | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +interface Token { | ||
| 19 | + accessToken: string; | ||
| 20 | + refreshToken: string; | ||
| 21 | + expiration: Date; | ||
| 22 | + user: { | ||
| 23 | + id: number; | ||
| 24 | + username: string; | ||
| 25 | + name: string; | ||
| 26 | + totalSize: number; | ||
| 27 | + currentSize: number; | ||
| 28 | + rootFolder: number; | ||
| 29 | + }; | ||
| 30 | +} | ||
| 31 | + | ||
| 32 | +export const TokenContext = React.createContext<ReturnType<typeof useAuth>>( | ||
| 33 | + {} as any | ||
| 34 | +); | ||
| 35 | + | ||
| 36 | +export function useAuth() { | ||
| 37 | + const [token, setToken] = useState<Token | null>(() => { | ||
| 38 | + const item = localStorage.getItem("token"); | ||
| 39 | + if (item) { | ||
| 40 | + const token = JSON.parse(item); | ||
| 41 | + token.expiration = new Date(token.expiration); | ||
| 42 | + return token; | ||
| 43 | + } | ||
| 44 | + return null; | ||
| 45 | + }); | ||
| 46 | + | ||
| 47 | + const login = useCallback( | ||
| 48 | + async (username: string, password: string, remember: boolean) => { | ||
| 49 | + const body = new URLSearchParams(); | ||
| 50 | + body.set("user_id", username); | ||
| 51 | + body.set("password", password); | ||
| 52 | + | ||
| 53 | + const response = await ky | ||
| 54 | + .post("/users/login/", { body }) | ||
| 55 | + .json<LoginResponse>(); | ||
| 56 | + | ||
| 57 | + const token = { | ||
| 58 | + accessToken: response.access, | ||
| 59 | + refreshToken: response.refresh, | ||
| 60 | + expiration: new Date(response.exp * 1000), | ||
| 61 | + user: { | ||
| 62 | + id: response.user.int_id, | ||
| 63 | + username: response.user.user_id, | ||
| 64 | + name: response.user.name, | ||
| 65 | + totalSize: response.user.total_size, | ||
| 66 | + currentSize: response.user.current_size, | ||
| 67 | + rootFolder: response.user.root_folder, | ||
| 68 | + }, | ||
| 69 | + }; | ||
| 70 | + | ||
| 71 | + setToken(token); | ||
| 72 | + | ||
| 73 | + if (remember) { | ||
| 74 | + localStorage.setItem("token", JSON.stringify(token)); | ||
| 75 | + } | ||
| 76 | + }, | ||
| 77 | + [] | ||
| 78 | + ); | ||
| 79 | + | ||
| 80 | + const logout = useCallback(() => setToken(null), []); | ||
| 81 | + | ||
| 82 | + return { token, login, logout }; | ||
| 83 | +} |
frontend/src/file/CreateFolderPopover.tsx
0 → 100644
| 1 | +import React, { useState } from "react"; | ||
| 2 | +import { Button, Input } from "antd"; | ||
| 3 | + | ||
| 4 | +export type CreateFolderPopoverProps = { | ||
| 5 | + onCreate: (name: string) => void; | ||
| 6 | + onCancel?: () => void; | ||
| 7 | +}; | ||
| 8 | + | ||
| 9 | +export function CreateFolderPopover({ | ||
| 10 | + onCreate, | ||
| 11 | + onCancel, | ||
| 12 | +}: CreateFolderPopoverProps) { | ||
| 13 | + const [name, setName] = useState<string>(""); | ||
| 14 | + return ( | ||
| 15 | + <div> | ||
| 16 | + <Input | ||
| 17 | + value={name} | ||
| 18 | + onChange={(event) => setName(event.target.value)} | ||
| 19 | + placeholder="이름" | ||
| 20 | + style={{ marginBottom: 10 }} | ||
| 21 | + /> | ||
| 22 | + <div className="ant-popover-buttons"> | ||
| 23 | + <Button size="small" onClick={onCancel}> | ||
| 24 | + 취소 | ||
| 25 | + </Button> | ||
| 26 | + <Button type="primary" size="small" onClick={() => onCreate(name)}> | ||
| 27 | + 생성 | ||
| 28 | + </Button> | ||
| 29 | + </div> | ||
| 30 | + </div> | ||
| 31 | + ); | ||
| 32 | +} |
File mode changed
frontend/src/file/FileItemActions.tsx
0 → 100644
| 1 | +import React, { useState, Fragment } from "react"; | ||
| 2 | +import { Popconfirm, Popover, Button, message } from "antd"; | ||
| 3 | +import { FileItem } from "./useFileList"; | ||
| 4 | +import styles from "./FileItemActions.module.scss"; | ||
| 5 | +import { FileListPopover } from "./FileListPopover"; | ||
| 6 | +import { FileRenamePopover } from "./FileRenamePopover"; | ||
| 7 | + | ||
| 8 | +export type FileItemActionsProps = { | ||
| 9 | + item: FileItem; | ||
| 10 | + onRename: (id: number, name: string) => void; | ||
| 11 | + onMove: (id: number, to: number) => void; | ||
| 12 | + onCopy: (id: number, to: number) => void; | ||
| 13 | + onDelete: (id: number) => void; | ||
| 14 | +}; | ||
| 15 | + | ||
| 16 | +export function FileItemActions({ | ||
| 17 | + item, | ||
| 18 | + onRename, | ||
| 19 | + onMove, | ||
| 20 | + onCopy, | ||
| 21 | + onDelete, | ||
| 22 | +}: FileItemActionsProps) { | ||
| 23 | + const [rename, setRename] = useState<boolean>(false); | ||
| 24 | + const [move, setMove] = useState<boolean>(false); | ||
| 25 | + const [copy, setCopy] = useState<boolean>(false); | ||
| 26 | + | ||
| 27 | + return ( | ||
| 28 | + <div className={styles.actions}> | ||
| 29 | + <Popover | ||
| 30 | + title="변경할 이름을 입력하세요" | ||
| 31 | + content={ | ||
| 32 | + <FileRenamePopover | ||
| 33 | + name={item.name} | ||
| 34 | + onRename={(name: string) => { | ||
| 35 | + if (name === item.name) { | ||
| 36 | + return message.error("동일한 이름으로는 변경할 수 없습니다"); | ||
| 37 | + } | ||
| 38 | + if (!name) { | ||
| 39 | + return message.error("변경할 이름을 입력하세요"); | ||
| 40 | + } | ||
| 41 | + onRename(item.id, name); | ||
| 42 | + setRename(false); | ||
| 43 | + }} | ||
| 44 | + onCancel={() => setRename(false)} | ||
| 45 | + /> | ||
| 46 | + } | ||
| 47 | + trigger="click" | ||
| 48 | + visible={rename} | ||
| 49 | + onVisibleChange={setRename} | ||
| 50 | + > | ||
| 51 | + <Button type="link" size="small"> | ||
| 52 | + 이름 변경 | ||
| 53 | + </Button> | ||
| 54 | + </Popover> | ||
| 55 | + {!item.is_folder && ( | ||
| 56 | + <Button type="link" size="small"> | ||
| 57 | + 공유 | ||
| 58 | + </Button> | ||
| 59 | + )} | ||
| 60 | + <Popover | ||
| 61 | + title="이동할 폴더를 선택하세요" | ||
| 62 | + content={ | ||
| 63 | + <FileListPopover | ||
| 64 | + root={item.parent} | ||
| 65 | + onSelect={(to: number) => { | ||
| 66 | + if (to === item.parent) { | ||
| 67 | + return message.error("같은 폴더로는 이동할 수 없습니다"); | ||
| 68 | + } | ||
| 69 | + onMove(item.id, to); | ||
| 70 | + setMove(false); | ||
| 71 | + }} | ||
| 72 | + onCancel={() => setMove(false)} | ||
| 73 | + /> | ||
| 74 | + } | ||
| 75 | + trigger="click" | ||
| 76 | + visible={move} | ||
| 77 | + onVisibleChange={setMove} | ||
| 78 | + > | ||
| 79 | + <Button type="link" size="small"> | ||
| 80 | + 이동 | ||
| 81 | + </Button> | ||
| 82 | + </Popover> | ||
| 83 | + {!item.is_folder && ( | ||
| 84 | + <Popover | ||
| 85 | + title="복사할 폴더를 선택하세요" | ||
| 86 | + content={ | ||
| 87 | + <FileListPopover | ||
| 88 | + root={item.parent} | ||
| 89 | + onSelect={(to: number) => { | ||
| 90 | + onCopy(item.id, to); | ||
| 91 | + setCopy(false); | ||
| 92 | + }} | ||
| 93 | + onCancel={() => setCopy(false)} | ||
| 94 | + /> | ||
| 95 | + } | ||
| 96 | + trigger="click" | ||
| 97 | + visible={copy} | ||
| 98 | + onVisibleChange={setCopy} | ||
| 99 | + > | ||
| 100 | + <Button type="link" size="small"> | ||
| 101 | + 복사 | ||
| 102 | + </Button> | ||
| 103 | + </Popover> | ||
| 104 | + )} | ||
| 105 | + {!item.is_folder && ( | ||
| 106 | + <Popconfirm | ||
| 107 | + title="정말로 삭제하시겠습니까?" | ||
| 108 | + onConfirm={() => onDelete(item.id)} | ||
| 109 | + okText="삭제" | ||
| 110 | + cancelText="취소" | ||
| 111 | + > | ||
| 112 | + <Button type="link" size="small"> | ||
| 113 | + 삭제 | ||
| 114 | + </Button> | ||
| 115 | + </Popconfirm> | ||
| 116 | + )} | ||
| 117 | + </div> | ||
| 118 | + ); | ||
| 119 | +} |
frontend/src/file/FileList.module.scss
0 → 100644
frontend/src/file/FileList.tsx
0 → 100644
| 1 | +import React, { useCallback, useState, useContext } from "react"; | ||
| 2 | +import { Table, message, Button, Popover } from "antd"; | ||
| 3 | +import { ColumnsType } from "antd/lib/table"; | ||
| 4 | +import filesize from "filesize"; | ||
| 5 | + | ||
| 6 | +import { useParams } from "react-router-dom"; | ||
| 7 | +import { useFileList, FileItem } from "./useFileList"; | ||
| 8 | +import { useApi } from "util/useApi"; | ||
| 9 | +import { FileListItem } from "./FileListItem"; | ||
| 10 | +import { FileItemActions } from "./FileItemActions"; | ||
| 11 | + | ||
| 12 | +import styles from "./FileList.module.scss"; | ||
| 13 | +import { FileUploadPopover } from "./FileUploadPopover"; | ||
| 14 | +import { CreateFolderPopover } from "./CreateFolderPopover"; | ||
| 15 | +import { TokenContext } from "auth/useAuth"; | ||
| 16 | + | ||
| 17 | +export function FileList() { | ||
| 18 | + const id = useParams<{ id: string }>().id; | ||
| 19 | + const { data, reload } = useFileList(id); | ||
| 20 | + | ||
| 21 | + const [upload, setUpload] = useState<boolean>(false); | ||
| 22 | + const [createFolder, setCreateFolder] = useState<boolean>(false); | ||
| 23 | + | ||
| 24 | + const { token } = useContext(TokenContext); | ||
| 25 | + const userId = token?.user.id || ""; | ||
| 26 | + | ||
| 27 | + const api = useApi(); | ||
| 28 | + | ||
| 29 | + const handleCreateFolder = useCallback( | ||
| 30 | + async (id: number, name: string) => { | ||
| 31 | + try { | ||
| 32 | + const body = new URLSearchParams(); | ||
| 33 | + body.set("name", name); | ||
| 34 | + | ||
| 35 | + await api.post(`/items/${id}/children/`, { | ||
| 36 | + searchParams: { | ||
| 37 | + user_id: userId, | ||
| 38 | + }, | ||
| 39 | + body, | ||
| 40 | + }); | ||
| 41 | + await reload(); | ||
| 42 | + | ||
| 43 | + message.info("폴더가 생성되었습니다"); | ||
| 44 | + } catch { | ||
| 45 | + message.error("폴더 생성에 실패했습니다"); | ||
| 46 | + } | ||
| 47 | + }, | ||
| 48 | + [api, reload, userId] | ||
| 49 | + ); | ||
| 50 | + | ||
| 51 | + const handleRename = useCallback( | ||
| 52 | + async (id: number, name: string) => { | ||
| 53 | + try { | ||
| 54 | + const body = new URLSearchParams(); | ||
| 55 | + body.set("name", name); | ||
| 56 | + | ||
| 57 | + await api.post(`/items/${id}/move/`, { body }); | ||
| 58 | + await reload(); | ||
| 59 | + | ||
| 60 | + message.info("이름이 변경되었습니다"); | ||
| 61 | + } catch { | ||
| 62 | + message.error("이름 변경에 실패했습니다"); | ||
| 63 | + } | ||
| 64 | + }, | ||
| 65 | + [api, reload] | ||
| 66 | + ); | ||
| 67 | + | ||
| 68 | + const handleMove = useCallback( | ||
| 69 | + async (id: number, to: number) => { | ||
| 70 | + try { | ||
| 71 | + const body = new URLSearchParams(); | ||
| 72 | + body.set("parent", to.toString(10)); | ||
| 73 | + | ||
| 74 | + await api.post(`/items/${id}/move/`, { body }); | ||
| 75 | + await reload(); | ||
| 76 | + | ||
| 77 | + message.info("이동되었습니다"); | ||
| 78 | + } catch { | ||
| 79 | + message.error("파일 이동에 실패했습니다"); | ||
| 80 | + } | ||
| 81 | + }, | ||
| 82 | + [api, reload] | ||
| 83 | + ); | ||
| 84 | + | ||
| 85 | + const handleCopy = useCallback( | ||
| 86 | + async (id: number, to: number) => { | ||
| 87 | + try { | ||
| 88 | + const body = new URLSearchParams(); | ||
| 89 | + body.set("parent", to.toString(10)); | ||
| 90 | + | ||
| 91 | + await api.post(`/items/${id}/copy/`, { body }); | ||
| 92 | + await reload(); | ||
| 93 | + | ||
| 94 | + message.info("복사되었습니다"); | ||
| 95 | + } catch { | ||
| 96 | + message.error("파일 복사에 실패했습니다"); | ||
| 97 | + } | ||
| 98 | + }, | ||
| 99 | + [api, reload] | ||
| 100 | + ); | ||
| 101 | + | ||
| 102 | + const handleDelete = useCallback( | ||
| 103 | + async (id: number) => { | ||
| 104 | + try { | ||
| 105 | + await api.delete(`/items/${id}/`); | ||
| 106 | + await reload(); | ||
| 107 | + message.info("삭제되었습니다"); | ||
| 108 | + } catch { | ||
| 109 | + message.error("파일 삭제에 실패했습니다"); | ||
| 110 | + } | ||
| 111 | + }, | ||
| 112 | + [api, reload] | ||
| 113 | + ); | ||
| 114 | + | ||
| 115 | + if (!data) { | ||
| 116 | + return null; | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + const list = [...data.list].sort((itemA, itemB) => | ||
| 120 | + itemA.is_folder === itemB.is_folder ? 0 : itemA.is_folder ? -1 : 1 | ||
| 121 | + ); | ||
| 122 | + | ||
| 123 | + if (data.parent !== null) { | ||
| 124 | + list.unshift(({ | ||
| 125 | + id: data.parent, | ||
| 126 | + is_folder: true, | ||
| 127 | + name: "..", | ||
| 128 | + file_type: "folder", | ||
| 129 | + } as unknown) as FileItem); | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + return ( | ||
| 133 | + <div> | ||
| 134 | + <div className={styles.header}> | ||
| 135 | + <div>{data.parent !== null && <h3>{data.name}</h3>}</div> | ||
| 136 | + <div> | ||
| 137 | + <Popover | ||
| 138 | + content={<FileUploadPopover root={data.id} reload={reload} />} | ||
| 139 | + trigger="click" | ||
| 140 | + visible={upload} | ||
| 141 | + onVisibleChange={setUpload} | ||
| 142 | + > | ||
| 143 | + <Button type="link" size="small"> | ||
| 144 | + 파일 업로드 | ||
| 145 | + </Button> | ||
| 146 | + </Popover> | ||
| 147 | + <Popover | ||
| 148 | + title="폴더 이름을 입력하세요" | ||
| 149 | + content={ | ||
| 150 | + <CreateFolderPopover | ||
| 151 | + onCreate={(name: string) => { | ||
| 152 | + if (!name) { | ||
| 153 | + return message.error("폴더 이름을 입력하세요"); | ||
| 154 | + } | ||
| 155 | + handleCreateFolder(data.id, name); | ||
| 156 | + setCreateFolder(false); | ||
| 157 | + }} | ||
| 158 | + onCancel={() => setCreateFolder(false)} | ||
| 159 | + /> | ||
| 160 | + } | ||
| 161 | + trigger="click" | ||
| 162 | + visible={createFolder} | ||
| 163 | + onVisibleChange={setCreateFolder} | ||
| 164 | + > | ||
| 165 | + <Button type="link" size="small"> | ||
| 166 | + 새 폴더 | ||
| 167 | + </Button> | ||
| 168 | + </Popover> | ||
| 169 | + </div> | ||
| 170 | + </div> | ||
| 171 | + <Table | ||
| 172 | + rowKey="id" | ||
| 173 | + columns={getColumns({ | ||
| 174 | + handleRename, | ||
| 175 | + handleMove, | ||
| 176 | + handleCopy, | ||
| 177 | + handleDelete, | ||
| 178 | + })} | ||
| 179 | + dataSource={list} | ||
| 180 | + pagination={false} | ||
| 181 | + locale={{ | ||
| 182 | + emptyText: "파일이 없습니다", | ||
| 183 | + }} | ||
| 184 | + /> | ||
| 185 | + </div> | ||
| 186 | + ); | ||
| 187 | +} | ||
| 188 | + | ||
| 189 | +type GetColumnsParams = { | ||
| 190 | + handleRename: (id: number, name: string) => void; | ||
| 191 | + handleMove: (id: number, to: number) => void; | ||
| 192 | + handleCopy: (id: number, to: number) => void; | ||
| 193 | + handleDelete: (id: number) => void; | ||
| 194 | +}; | ||
| 195 | + | ||
| 196 | +function getColumns({ | ||
| 197 | + handleRename, | ||
| 198 | + handleMove, | ||
| 199 | + handleCopy, | ||
| 200 | + handleDelete, | ||
| 201 | +}: GetColumnsParams): ColumnsType<FileItem> { | ||
| 202 | + return [ | ||
| 203 | + { | ||
| 204 | + title: "이름", | ||
| 205 | + key: "name", | ||
| 206 | + dataIndex: "name", | ||
| 207 | + render: (_name: string, item) => <FileListItem item={item} />, | ||
| 208 | + }, | ||
| 209 | + { | ||
| 210 | + title: "크기", | ||
| 211 | + key: "size", | ||
| 212 | + dataIndex: "size", | ||
| 213 | + width: 120, | ||
| 214 | + render: (bytes: number, item) => | ||
| 215 | + item.is_folder ? "-" : filesize(bytes, { round: 0 }), | ||
| 216 | + }, | ||
| 217 | + { | ||
| 218 | + title: "", | ||
| 219 | + key: "action", | ||
| 220 | + dataIndex: "", | ||
| 221 | + width: 300, | ||
| 222 | + render: (__: any, item) => ( | ||
| 223 | + <FileItemActions | ||
| 224 | + item={item} | ||
| 225 | + onRename={handleRename} | ||
| 226 | + onMove={handleMove} | ||
| 227 | + onCopy={handleCopy} | ||
| 228 | + onDelete={handleDelete} | ||
| 229 | + /> | ||
| 230 | + ), | ||
| 231 | + }, | ||
| 232 | + ]; | ||
| 233 | +} |
frontend/src/file/FileListItem.tsx
0 → 100644
| 1 | +import React from "react"; | ||
| 2 | +import { FileItem } from "./useFileList"; | ||
| 3 | +import { Link } from "react-router-dom"; | ||
| 4 | +import { Button } from "antd"; | ||
| 5 | + | ||
| 6 | +import { FolderFilled, FileFilled } from "@ant-design/icons"; | ||
| 7 | +import { useDownload } from "./useDownload"; | ||
| 8 | + | ||
| 9 | +export function FileListItem({ item }: { item: FileItem }) { | ||
| 10 | + const download = useDownload(); | ||
| 11 | + return item.is_folder ? ( | ||
| 12 | + <Link | ||
| 13 | + className="ant-btn ant-btn-link ant-btn-sm" | ||
| 14 | + to={`/folder/${item.id}`} | ||
| 15 | + style={{ padding: 0, color: "#001529" }} | ||
| 16 | + > | ||
| 17 | + <FolderFilled /> <span>{item.name}</span> | ||
| 18 | + </Link> | ||
| 19 | + ) : ( | ||
| 20 | + <Button | ||
| 21 | + type="link" | ||
| 22 | + size="small" | ||
| 23 | + onClick={() => download(item.id)} | ||
| 24 | + style={{ padding: 0, color: "#001529" }} | ||
| 25 | + > | ||
| 26 | + <FileFilled /> {item.name} | ||
| 27 | + </Button> | ||
| 28 | + ); | ||
| 29 | +} |
frontend/src/file/FileListPopover.tsx
0 → 100644
| 1 | +import React, { useState } from "react"; | ||
| 2 | +import { useFileList } from "./useFileList"; | ||
| 3 | +import { Button } from "antd"; | ||
| 4 | + | ||
| 5 | +import styles from "./FileListPopover.module.scss"; | ||
| 6 | + | ||
| 7 | +export type FileListPopoverProps = { | ||
| 8 | + root: number; | ||
| 9 | + onSelect: (id: number) => void; | ||
| 10 | + onCancel?: () => void; | ||
| 11 | +}; | ||
| 12 | + | ||
| 13 | +export function FileListPopover({ | ||
| 14 | + root, | ||
| 15 | + onSelect, | ||
| 16 | + onCancel, | ||
| 17 | +}: FileListPopoverProps) { | ||
| 18 | + const [id, setId] = useState<number>(root); | ||
| 19 | + const { data } = useFileList(id); | ||
| 20 | + | ||
| 21 | + if (!data) { | ||
| 22 | + return null; | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + const list = data.list | ||
| 26 | + .filter((item) => item.is_folder) | ||
| 27 | + .map((item) => ({ id: item.id, name: item.name })); | ||
| 28 | + | ||
| 29 | + if (data.parent !== null) { | ||
| 30 | + list.unshift({ id: data.parent, name: ".." }); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + return ( | ||
| 34 | + <div> | ||
| 35 | + <div>{data.name}</div> | ||
| 36 | + <ul className={styles.list}> | ||
| 37 | + {list.map((item) => ( | ||
| 38 | + <li key={item.id}> | ||
| 39 | + <Button type="link" size="small" onClick={() => setId(item.id)}> | ||
| 40 | + {item.name} | ||
| 41 | + </Button> | ||
| 42 | + </li> | ||
| 43 | + ))} | ||
| 44 | + </ul> | ||
| 45 | + <div className="ant-popover-buttons"> | ||
| 46 | + <Button size="small" onClick={onCancel}> | ||
| 47 | + 취소 | ||
| 48 | + </Button> | ||
| 49 | + <Button type="primary" size="small" onClick={() => onSelect(id)}> | ||
| 50 | + 선택 | ||
| 51 | + </Button> | ||
| 52 | + </div> | ||
| 53 | + </div> | ||
| 54 | + ); | ||
| 55 | +} |
frontend/src/file/FileRenamePopover.tsx
0 → 100644
| 1 | +import React, { useState } from "react"; | ||
| 2 | +import { Button, Input } from "antd"; | ||
| 3 | + | ||
| 4 | +export type FileRenamePopoverProps = { | ||
| 5 | + name: string; | ||
| 6 | + onRename: (name: string) => void; | ||
| 7 | + onCancel?: () => void; | ||
| 8 | +}; | ||
| 9 | + | ||
| 10 | +export function FileRenamePopover({ | ||
| 11 | + name: oldName, | ||
| 12 | + onRename, | ||
| 13 | + onCancel, | ||
| 14 | +}: FileRenamePopoverProps) { | ||
| 15 | + const [name, setName] = useState<string>(oldName); | ||
| 16 | + return ( | ||
| 17 | + <div> | ||
| 18 | + <Input | ||
| 19 | + value={name} | ||
| 20 | + onChange={(event) => setName(event.target.value)} | ||
| 21 | + placeholder="이름" | ||
| 22 | + style={{ marginBottom: 10 }} | ||
| 23 | + /> | ||
| 24 | + <div className="ant-popover-buttons"> | ||
| 25 | + <Button size="small" onClick={onCancel}> | ||
| 26 | + 취소 | ||
| 27 | + </Button> | ||
| 28 | + <Button type="primary" size="small" onClick={() => onRename(name)}> | ||
| 29 | + 변경 | ||
| 30 | + </Button> | ||
| 31 | + </div> | ||
| 32 | + </div> | ||
| 33 | + ); | ||
| 34 | +} |
frontend/src/file/FileUploadPopover.tsx
0 → 100644
| 1 | +import React, { useCallback, useRef } from "react"; | ||
| 2 | +import Dragger from "antd/lib/upload/Dragger"; | ||
| 3 | +import { InboxOutlined } from "@ant-design/icons"; | ||
| 4 | +import { useApi } from "util/useApi"; | ||
| 5 | + | ||
| 6 | +export type FileUploadPopoverProps = { | ||
| 7 | + root: number; | ||
| 8 | + reload: () => void; | ||
| 9 | +}; | ||
| 10 | + | ||
| 11 | +export function FileUploadPopover({ root, reload }: FileUploadPopoverProps) { | ||
| 12 | + const api = useApi(); | ||
| 13 | + const fields = useRef<any>(); | ||
| 14 | + const stateMap = useRef<Record<string, number>>({}); | ||
| 15 | + | ||
| 16 | + const getS3Object = useCallback( | ||
| 17 | + async (file: File) => { | ||
| 18 | + const body = new URLSearchParams(); | ||
| 19 | + body.set("name", file.name); | ||
| 20 | + body.set("size", file.size.toString()); | ||
| 21 | + | ||
| 22 | + const response = await api | ||
| 23 | + .post(`/items/${root}/upload/`, { body }) | ||
| 24 | + .json<any>(); | ||
| 25 | + | ||
| 26 | + stateMap.current[file.name] = response.item.id; | ||
| 27 | + fields.current = response.signed_url.fields; | ||
| 28 | + return response.signed_url.url; | ||
| 29 | + }, | ||
| 30 | + [api, root] | ||
| 31 | + ); | ||
| 32 | + | ||
| 33 | + const setObjectStatus = useCallback( | ||
| 34 | + async (info) => { | ||
| 35 | + if (info.file.status === "done") { | ||
| 36 | + const id = stateMap.current[info.file.name]; | ||
| 37 | + if (typeof id !== "undefined") { | ||
| 38 | + const body = new URLSearchParams(); | ||
| 39 | + body.set("item_id", id.toString()); | ||
| 40 | + await api.post(`/items/${id}/status/`, { body }); | ||
| 41 | + reload(); | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | + }, | ||
| 45 | + [api, reload] | ||
| 46 | + ); | ||
| 47 | + | ||
| 48 | + return ( | ||
| 49 | + <Dragger | ||
| 50 | + name="file" | ||
| 51 | + multiple={true} | ||
| 52 | + action={getS3Object} | ||
| 53 | + data={() => fields.current} | ||
| 54 | + onChange={setObjectStatus} | ||
| 55 | + style={{ padding: 40 }} | ||
| 56 | + > | ||
| 57 | + <p className="ant-upload-drag-icon"> | ||
| 58 | + <InboxOutlined /> | ||
| 59 | + </p> | ||
| 60 | + <p className="ant-upload-text"> | ||
| 61 | + 업로드할 파일을 선택하거나 드래그 하세요 | ||
| 62 | + </p> | ||
| 63 | + <p className="ant-upload-hint"></p> | ||
| 64 | + </Dragger> | ||
| 65 | + ); | ||
| 66 | +} |
frontend/src/file/useDownload.ts
0 → 100644
| 1 | +import { useApi } from "util/useApi"; | ||
| 2 | +import { useCallback } from "react"; | ||
| 3 | + | ||
| 4 | +function downloadURL(url: string, name: string) { | ||
| 5 | + const link = document.createElement("a"); | ||
| 6 | + link.setAttribute("download", name); | ||
| 7 | + link.href = url; | ||
| 8 | + link.click(); | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +export function useDownload() { | ||
| 12 | + const api = useApi(); | ||
| 13 | + const download = useCallback( | ||
| 14 | + async (id: number) => { | ||
| 15 | + const response = await api.get(`/items/${id}/`).json<any>(); | ||
| 16 | + const { signed_url, name } = response.data; | ||
| 17 | + downloadURL(signed_url, name); | ||
| 18 | + }, | ||
| 19 | + [api] | ||
| 20 | + ); | ||
| 21 | + return download; | ||
| 22 | +} |
frontend/src/file/useFileList.ts
0 → 100644
| 1 | +import { useState, useCallback, useEffect } from "react"; | ||
| 2 | +import ky from "ky"; | ||
| 3 | + | ||
| 4 | +interface FileListData extends FileItem { | ||
| 5 | + list: FileItem[]; | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +interface FileListResponse { | ||
| 9 | + data: FileListData; | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +export interface FileItem { | ||
| 13 | + is_folder: boolean; | ||
| 14 | + name: string; | ||
| 15 | + file_type: "folder" | "file"; | ||
| 16 | + path: string; | ||
| 17 | + parent: number; | ||
| 18 | + user_id: number; | ||
| 19 | + size: number; | ||
| 20 | + is_deleted: boolean; | ||
| 21 | + created_time: string | null; | ||
| 22 | + updated_time: string; | ||
| 23 | + status: boolean; | ||
| 24 | + id: number; | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +export function useFileList(id: string | number) { | ||
| 28 | + const [data, setData] = useState<FileListData | null>(null); | ||
| 29 | + | ||
| 30 | + const reload = useCallback(async () => { | ||
| 31 | + const response = await ky | ||
| 32 | + .get(`/items/${id}/children/`) | ||
| 33 | + .json<FileListResponse>(); | ||
| 34 | + | ||
| 35 | + setData(response.data); | ||
| 36 | + }, [id]); | ||
| 37 | + | ||
| 38 | + useEffect(() => { | ||
| 39 | + reload(); | ||
| 40 | + }, [reload]); | ||
| 41 | + | ||
| 42 | + return { data, reload }; | ||
| 43 | +} |
| 1 | -body { | 1 | +#root { |
| 2 | - margin: 0; | 2 | + height: 100%; |
| 3 | - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", | ||
| 4 | - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", | ||
| 5 | - sans-serif; | ||
| 6 | - -webkit-font-smoothing: antialiased; | ||
| 7 | - -moz-osx-font-smoothing: grayscale; | ||
| 8 | -} | ||
| 9 | - | ||
| 10 | -code { | ||
| 11 | - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", | ||
| 12 | - monospace; | ||
| 13 | } | 3 | } | ... | ... |
| 1 | import React from "react"; | 1 | import React from "react"; |
| 2 | import ReactDOM from "react-dom"; | 2 | import ReactDOM from "react-dom"; |
| 3 | +import { BrowserRouter } from "react-router-dom"; | ||
| 3 | 4 | ||
| 5 | +import "antd/dist/antd.css"; | ||
| 4 | import "./index.css"; | 6 | import "./index.css"; |
| 5 | 7 | ||
| 6 | import { App } from "./App"; | 8 | import { App } from "./App"; |
| ... | @@ -8,9 +10,9 @@ import { App } from "./App"; | ... | @@ -8,9 +10,9 @@ import { App } from "./App"; |
| 8 | import * as serviceWorker from "./serviceWorker"; | 10 | import * as serviceWorker from "./serviceWorker"; |
| 9 | 11 | ||
| 10 | ReactDOM.render( | 12 | ReactDOM.render( |
| 11 | - <React.StrictMode> | 13 | + <BrowserRouter> |
| 12 | <App /> | 14 | <App /> |
| 13 | - </React.StrictMode>, | 15 | + </BrowserRouter>, |
| 14 | document.getElementById("root") | 16 | document.getElementById("root") |
| 15 | ); | 17 | ); |
| 16 | 18 | ... | ... |
frontend/src/layout/Page.module.scss
0 → 100644
| 1 | +.layout { | ||
| 2 | + height: 100%; | ||
| 3 | +} | ||
| 4 | + | ||
| 5 | +.header { | ||
| 6 | + display: flex; | ||
| 7 | + align-items: center; | ||
| 8 | + justify-content: space-between; | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +.content { | ||
| 12 | + background: #fff; | ||
| 13 | + padding: 25px 50px; | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +.logo { | ||
| 17 | + width: 120px; | ||
| 18 | + height: 31px; | ||
| 19 | + margin: 16px 24px 16px 0; | ||
| 20 | + float: left; | ||
| 21 | + display: flex; | ||
| 22 | + align-items: center; | ||
| 23 | + justify-content: center; | ||
| 24 | + color: white; | ||
| 25 | + font-size: 24px; | ||
| 26 | + font-weight: bold; | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +.user { | ||
| 30 | + display: flex; | ||
| 31 | + align-items: center; | ||
| 32 | + color: white; | ||
| 33 | + | ||
| 34 | + svg { | ||
| 35 | + width: 28px; | ||
| 36 | + height: 28px; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + &:hover, | ||
| 40 | + &:active, | ||
| 41 | + &:focus { | ||
| 42 | + color: rgba(255, 255, 255, 0.65); | ||
| 43 | + } | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +.footer { | ||
| 47 | + text-align: center; | ||
| 48 | +} |
frontend/src/layout/Page.tsx
0 → 100644
| 1 | +import React, { useContext } from "react"; | ||
| 2 | +import { Layout, Popover, Button } from "antd"; | ||
| 3 | +import { UserOutlined } from "@ant-design/icons"; | ||
| 4 | +import { TokenContext } from "auth/useAuth"; | ||
| 5 | + | ||
| 6 | +import styles from "./Page.module.scss"; | ||
| 7 | + | ||
| 8 | +export function Page({ children }: { children: React.ReactNode }) { | ||
| 9 | + const { token, logout } = useContext(TokenContext); | ||
| 10 | + return ( | ||
| 11 | + <Layout className={styles.layout}> | ||
| 12 | + <Layout.Header className={styles.header}> | ||
| 13 | + <div className={styles.logo}>KHUDrive</div> | ||
| 14 | + <Popover | ||
| 15 | + content={ | ||
| 16 | + <div> | ||
| 17 | + {token?.user.name} | ||
| 18 | + <Button type="link" onClick={logout}> | ||
| 19 | + 로그아웃 | ||
| 20 | + </Button> | ||
| 21 | + </div> | ||
| 22 | + } | ||
| 23 | + trigger="click" | ||
| 24 | + > | ||
| 25 | + <Button type="text" className={styles.user}> | ||
| 26 | + <UserOutlined /> | ||
| 27 | + </Button> | ||
| 28 | + </Popover> | ||
| 29 | + </Layout.Header> | ||
| 30 | + <Layout.Content className={styles.content}>{children}</Layout.Content> | ||
| 31 | + <Layout.Footer className={styles.footer}> | ||
| 32 | + © 2020 Cloud Computing Team C | ||
| 33 | + </Layout.Footer> | ||
| 34 | + </Layout> | ||
| 35 | + ); | ||
| 36 | +} |
frontend/src/util/useApi.ts
0 → 100644
frontend/src/util/usePrevious.ts
0 → 100644
secrets.json
0 → 100644
| 1 | +{ | ||
| 2 | + "AWS_SESSION_TOKEN": "", | ||
| 3 | + "AWS_SECRET_ACCESS_KEY": "secret_key", | ||
| 4 | + "AWS_ACCESS_KEY_ID": "access_key", | ||
| 5 | + "AWS_REGION": "us-west-2", | ||
| 6 | + "AWS_STORAGE_BUCKET_NAME": "bucket", | ||
| 7 | + "AWS_ENDPOINT_URL": "http://localhost:39000" | ||
| 8 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or login to post a comment