김재형

Merge branch 'feature/frontend'

Showing 48 changed files with 1281 additions and 90 deletions
1 +/env
2 +/docker
3 +/.vscode
4 +.DS_Store
5 +__pycache__
...\ No newline at end of file ...\ No newline at end of file
...@@ -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(
187 + 's3',
188 + region_name=AWS_REGION,
177 aws_access_key_id=AWS_ACCESS_KEY_ID, 189 aws_access_key_id=AWS_ACCESS_KEY_ID,
178 aws_secret_access_key=AWS_SECRET_ACCESS_KEY, 190 aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
179 aws_session_token=AWS_SESSION_TOKEN, 191 aws_session_token=AWS_SESSION_TOKEN,
180 - config=Config(signature_version='s3v4')) 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)
244 - if parent != None and parent.is_folder == True:
245 child = get_object_or_None(Item, item_id=pk) 257 child = get_object_or_None(Item, item_id=pk)
258 +
246 if child == None: 259 if child == None:
247 return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT) 260 return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT)
261 +
262 + if parent_id != '':
263 + parent = get_object_or_None(Item, item_id=parent_id)
264 +
265 + if parent == None:
266 + return Response({'message': 'parent is not existed.'}, status=status.HTTP_200_OK)
267 + if parent.is_folder == False:
268 + return Response({'message': 'parent is not folder.'}, status=status.HTTP_200_OK)
269 +
270 + if parent != None and parent.is_folder == True:
248 child.parent = parent_id 271 child.parent = parent_id
272 + else:
273 + parent_id = child.parent
274 +
275 + if name != '':
276 + child.name = name;
277 +
249 child.save() 278 child.save()
250 - child = Item.objects.filter(item_id=pk) 279 + child = Item.objects.filter(item_id = pk)
251 child_data = serializers.serialize("json", child) 280 child_data = serializers.serialize("json", child)
252 json_child = json.loads(child_data) 281 json_child = json.loads(child_data)
253 res = json_child[0]['fields'] 282 res = json_child[0]['fields']
254 res['id'] = pk 283 res['id'] = pk
255 - parent = Item.objects.filter(item_id=parent_id) 284 + parent = Item.objects.filter(item_id = parent_id)
256 parent_data = serializers.serialize("json", parent) 285 parent_data = serializers.serialize("json", parent)
257 json_parent = json.loads(parent_data)[0]['fields'] 286 json_parent = json.loads(parent_data)[0]['fields']
258 res['parentInfo'] = json_parent 287 res['parentInfo'] = json_parent
288 +
259 return Response({'data': res}, status=status.HTTP_200_OK) 289 return Response({'data': res}, status=status.HTTP_200_OK)
260 - if parent == None:
261 - return Response({'message': 'parent is not existed.'}, status=status.HTTP_200_OK)
262 - if parent.is_folder == False:
263 - return Response({'message': 'parent is not folder.'}, status=status.HTTP_200_OK)
264 - return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT)
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):
...@@ -308,7 +333,7 @@ class ItemViewSet(viewsets.ViewSet): ...@@ -308,7 +333,7 @@ class ItemViewSet(viewsets.ViewSet):
308 url_path='children', url_name='children') 333 url_path='children', url_name='children')
309 def children(self, request, pk): 334 def children(self, request, pk):
310 if request.method == 'GET': 335 if request.method == 'GET':
311 - children = Item.objects.filter(parent=pk, is_deleted=False) 336 + children = Item.objects.filter(parent=pk, is_deleted=False, status=True)
312 children_data = serializers.serialize("json", children) 337 children_data = serializers.serialize("json", children)
313 json_children = json.loads(children_data) 338 json_children = json.loads(children_data)
314 parent = Item.objects.filter(item_id=pk) # item 339 parent = Item.objects.filter(item_id=pk) # item
...@@ -359,7 +384,15 @@ class ItemViewSet(viewsets.ViewSet): ...@@ -359,7 +384,15 @@ class ItemViewSet(viewsets.ViewSet):
359 url_path='upload', url_name='upload') 384 url_path='upload', url_name='upload')
360 def upload(self, request, pk): 385 def upload(self, request, pk):
361 if request.method == 'POST': 386 if request.method == 'POST':
362 - s3 = boto3.client('s3') 387 + s3 = boto3.client(
388 + 's3',
389 + region_name=AWS_REGION,
390 + aws_access_key_id=AWS_ACCESS_KEY_ID,
391 + aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
392 + aws_session_token=AWS_SESSION_TOKEN,
393 + endpoint_url=AWS_ENDPOINT_URL or None,
394 + config=Config(s3={'addressing_style': 'path'})
395 + )
363 s3_bucket = AWS_STORAGE_BUCKET_NAME 396 s3_bucket = AWS_STORAGE_BUCKET_NAME
364 397
365 # 파일 객체 생성 398 # 파일 객체 생성
...@@ -378,6 +411,7 @@ class ItemViewSet(viewsets.ViewSet): ...@@ -378,6 +411,7 @@ class ItemViewSet(viewsets.ViewSet):
378 { 411 {
379 "acl": "private", 412 "acl": "private",
380 "Content-Type": file_type, 413 "Content-Type": file_type,
414 + "Content-Disposition": "attachment",
381 'region': AWS_REGION, 415 'region': AWS_REGION,
382 'x-amz-algorithm': 'AWS4-HMAC-SHA256', 416 'x-amz-algorithm': 'AWS4-HMAC-SHA256',
383 'x-amz-date': date_long 417 'x-amz-date': date_long
...@@ -385,18 +419,26 @@ class ItemViewSet(viewsets.ViewSet): ...@@ -385,18 +419,26 @@ class ItemViewSet(viewsets.ViewSet):
385 [ 419 [
386 {"acl": "private"}, 420 {"acl": "private"},
387 {"Content-Type": file_type}, 421 {"Content-Type": file_type},
422 + {"Content-Disposition": "attachment"},
388 {'x-amz-algorithm': 'AWS4-HMAC-SHA256'}, 423 {'x-amz-algorithm': 'AWS4-HMAC-SHA256'},
389 {'x-amz-date': date_long} 424 {'x-amz-date': date_long}
390 ], 425 ],
391 3600 426 3600
392 ) 427 )
393 428
429 + item = Item.objects.filter(item_id=upload_item.item_id)
430 + item_data = serializers.serialize("json", item)
431 + json_item = json.loads(item_data)
432 + res = json_item[0]['fields']
433 + res['id'] = json_item[0]['pk']
434 +
394 data = { 435 data = {
395 "signed_url": presigned_post, 436 "signed_url": presigned_post,
396 - 'url': 'https://%s.s3.amazonaws.com/%s' % (s3_bucket, file_name) 437 + 'url': '%s/%s' % (presigned_post["url"], file_name),
438 + 'item': res
397 } 439 }
398 440
399 - return Response({'presigned_post': presigned_post, 'proc_data': data}, status=status.HTTP_200_OK) 441 + return Response(data, status=status.HTTP_200_OK)
400 442
401 # url: /status/ 443 # url: /status/
402 @action(methods=['POST'], detail=True, permission_classes=[AllowAny], 444 @action(methods=['POST'], detail=True, permission_classes=[AllowAny],
......
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 }
......
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<browserconfig>
3 + <msapplication>
4 + <tile>
5 + <square150x150logo src="/mstile-150x150.png"/>
6 + <TileColor>#da532c</TileColor>
7 + </tile>
8 + </msapplication>
9 +</browserconfig>
...@@ -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>
......
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>
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 }
......
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 +}
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 +}
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 +}
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 +}
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 +}
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 +}
1 +.header {
2 + display: flex;
3 + justify-content: space-between;
4 + margin-bottom: 20px;
5 +}
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 +}
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 +}
1 +.list {
2 + list-style: none;
3 + padding-left: 0;
4 +}
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 +}
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 +}
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 +}
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 +}
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
......
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 +}
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 +}
1 +import { useMemo } from "react";
2 +import ky from "ky";
3 +
4 +// TODO: Implement Auth
5 +export function useApi() {
6 + return useMemo(() => {
7 + return ky.extend({
8 + hooks: {
9 + beforeRequest: [],
10 + },
11 + });
12 + }, []);
13 +}
1 +import { useRef, useEffect } from "react";
2 +
3 +export function usePrevious<T>(value: T) {
4 + const ref = useRef<T>();
5 + useEffect(() => {
6 + ref.current = value;
7 + });
8 + return ref.current as T;
9 +}
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