diff --git a/convert_result/09c953a0.pdf b/convert_result/09c953a0.pdf new file mode 100644 index 0000000..0a0982c Binary files /dev/null and b/convert_result/09c953a0.pdf differ diff --git a/convert_result/09c953a0.xml b/convert_result/09c953a0.xml new file mode 100644 index 0000000..0e76b55 --- /dev/null +++ b/convert_result/09c953a0.xml @@ -0,0 +1,714 @@ + + + + + 09c953a0 + + 09c953a0 + + Music21 + + 2025-05-19 + music21 v.9.5.0 + + + + + 7 + 40 + + + + + + + + + + + + + 10080 + + 4 + + + + G + 2 + + + + + E + 4 + + 10080 + quarter + + single + + + + + + E + 4 + + 5040 + eighth + up + begin + + + + + + + E + 4 + + 5040 + eighth + up + end + + + + + + + E + 4 + + 10080 + quarter + + single + + + + + + E + 4 + + 10080 + quarter + + single + + + + + + + + + G + 1 + 4 + + 10080 + quarter + + single + + + + + + B + 4 + + 5040 + eighth + + single + + + + + + B + 4 + + 5040 + eighth + + single + + + + + + G + 1 + 4 + + 10080 + quarter + + single + + + + + + E + 4 + + 10080 + quarter + + + + + + + B + 4 + + 5040 + eighth + + single + + + + + + B + 4 + + 5040 + eighth + + + + + + + G + 1 + 4 + + 5040 + eighth + + single + + + + + + 5040 + eighth + + + + B + 4 + + 5040 + eighth + + single + + + + + + B + 4 + + 5040 + eighth + + + + + + + G + 1 + 4 + + 5040 + eighth + + single + + + + + + 5040 + eighth + + + + + + + E + 4 + + 10080 + quarter + + single + + + + + + E + 4 + + 10080 + quarter + + single + J + + + + + E + 4 + + 10080 + quarter + + single + + + + + + 10080 + quarter + + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + G + 1 + 4 + + 10080 + quarter + + single + + + + + + E + 4 + + 10080 + quarter + + single + + + + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + B + 4 + + 20160 + half + + single + + + + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + B + 4 + + 10080 + quarter + + + + + + + G + 1 + 4 + + 10080 + quarter + + single + + + + + + E + 4 + + 10080 + quarter + + single + + + + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + 10080 + quarter + + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + G + 1 + 4 + + 10080 + quarter + + single + + + + + + E + 4 + + 10080 + quarter + + single + + + + + + + + + B + 4 + + 5040 + eighth + down + begin + + + + + + + B + 4 + + 5040 + eighth + down + end + + single + + + + + + B + 4 + + 5040 + eighth + down + begin + + single + + + + + + C + 1 + 5 + + 5040 + eighth + down + end + + single + + + + + + B + 4 + + 10080 + quarter + + single + + + + + + 10080 + quarter + + + + + + + E + 5 + + 10080 + quarter + + single + + + + + + B + 4 + + 10080 + quarter + + + + + + + F + 1 + 5 + + 10080 + quarter + + single + + + + + + B + 4 + + 10080 + quarter + + + + + + + G + 1 + 4 + + 10080 + quarter + + single + + + + + + F + 1 + 4 + + 10080 + quarter + + single + + + + + + E + 4 + + 10080 + quarter + + + + + + + 10080 + quarter + + + light-light + + + + + + + 40320 + + + + \ No newline at end of file diff --git a/convert_result/35f4eef0-ce39-41fb-9a14-a4fb6c85ad29.pdf b/convert_result/35f4eef0-ce39-41fb-9a14-a4fb6c85ad29.pdf new file mode 100644 index 0000000..67252ec Binary files /dev/null and b/convert_result/35f4eef0-ce39-41fb-9a14-a4fb6c85ad29.pdf differ diff --git a/convert_result/35f4eef0-ce39-41fb-9a14-a4fb6c85ad29.xml b/convert_result/35f4eef0-ce39-41fb-9a14-a4fb6c85ad29.xml new file mode 100644 index 0000000..5e31ca6 --- /dev/null +++ b/convert_result/35f4eef0-ce39-41fb-9a14-a4fb6c85ad29.xml @@ -0,0 +1,783 @@ + + + + + 35f4eef0-ce39-41fb-9a14-a4fb6c85ad29 + + 35f4eef0-ce39-41fb-9a14-a4fb6c85ad29 + + Music21 + + 2025-05-19 + music21 v.9.5.0 + + + + + 7 + 40 + + + + + + + + + + + + + 10080 + + 9 + + + + G + 2 + + + + + E + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + E + -1 + 4 + + 5040 + eighth + up + begin + + + + + + + E + -1 + 4 + + 5040 + eighth + up + end + + + + + + + E + -1 + 4 + + 10080 + quarter + + single + + + + + + E + -1 + 4 + + 10080 + quarter + + single + + + + + + + + + G + 0 + 4 + + 10080 + quarter + natural + + single + + + + + + B + -1 + 4 + + 5040 + eighth + flat + + single + + + + + + B + -1 + 4 + + 5040 + eighth + + single + + + + + + G + 0 + 4 + + 10080 + quarter + natural + + single + + + + + + E + -1 + 4 + + 10080 + quarter + flat + + + + + + + B + -1 + 4 + + 5040 + eighth + flat + + single + + + + + + B + -1 + 4 + + 5040 + eighth + + + + + + + G + 0 + 4 + + 5040 + eighth + natural + + single + + + + + + 5040 + eighth + + + + B + -1 + 4 + + 5040 + eighth + flat + + single + + + + + + B + -1 + 4 + + 5040 + eighth + + + + + + + G + 0 + 4 + + 5040 + eighth + natural + + single + + + + + + 5040 + eighth + + + + + + + E + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + E + -1 + 4 + + 10080 + quarter + + single + J + + + + + E + -1 + 4 + + 10080 + quarter + + single + + + + + + 10080 + quarter + + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + B + -1 + 4 + + 10080 + quarter + + single + + + + + + G + 0 + 4 + + 10080 + quarter + natural + + single + + + + + + E + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + B + -1 + 4 + + 10080 + quarter + + single + + + + + + B + -1 + 4 + + 20160 + half + + single + + + + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + B + -1 + 4 + + 10080 + quarter + + + + + + + G + 0 + 4 + + 10080 + quarter + natural + + single + + + + + + E + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + B + -1 + 4 + + 10080 + quarter + + single + + + + + + B + -1 + 4 + + 10080 + quarter + + single + + + + + + 10080 + quarter + + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + B + -1 + 4 + + 10080 + quarter + + single + + + + + + G + 0 + 4 + + 10080 + quarter + natural + + single + + + + + + E + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + + + + B + -1 + 4 + + 5040 + eighth + flat + down + begin + + + + + + + B + -1 + 4 + + 5040 + eighth + down + end + + single + + + + + + B + -1 + 4 + + 5040 + eighth + down + begin + + single + + + + + + C + 0 + 5 + + 5040 + eighth + natural + down + end + + single + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + 10080 + quarter + + + + + + + E + -1 + 5 + + 10080 + quarter + flat + + single + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + + + + + + F + 0 + 5 + + 10080 + quarter + natural + + single + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + + + + + + G + 0 + 4 + + 10080 + quarter + natural + + single + + + + + + F + 0 + 4 + + 10080 + quarter + natural + + single + + + + + + E + -1 + 4 + + 10080 + quarter + flat + + + + + + + 10080 + quarter + + + light-light + + + + + + + 40320 + + + + \ No newline at end of file diff --git a/src/services/mypage_service.py b/convert_result/98f55ceb-b3a2-4b39-bbba-1768dcee6396.txt similarity index 100% rename from src/services/mypage_service.py rename to convert_result/98f55ceb-b3a2-4b39-bbba-1768dcee6396.txt diff --git a/convert_result/afbe7119-2470-42f5-a7cb-c5a0a144ac51.pdf b/convert_result/afbe7119-2470-42f5-a7cb-c5a0a144ac51.pdf new file mode 100644 index 0000000..389ed86 Binary files /dev/null and b/convert_result/afbe7119-2470-42f5-a7cb-c5a0a144ac51.pdf differ diff --git a/convert_result/afbe7119-2470-42f5-a7cb-c5a0a144ac51.xml b/convert_result/afbe7119-2470-42f5-a7cb-c5a0a144ac51.xml new file mode 100644 index 0000000..5b583ce --- /dev/null +++ b/convert_result/afbe7119-2470-42f5-a7cb-c5a0a144ac51.xml @@ -0,0 +1,761 @@ + + + + + afbe7119-2470-42f5-a7cb-c5a0a144ac51 + + afbe7119-2470-42f5-a7cb-c5a0a144ac51 + + Music21 + + 2025-05-19 + music21 v.9.5.0 + + + + + 7 + 40 + + + + + + + + + + + + + 10080 + + 6 + + + + G + 2 + + + + + F + 1 + 4 + + 10080 + quarter + + single + + + + + + F + 1 + 4 + + 5040 + eighth + up + begin + + + + + + + F + 1 + 4 + + 5040 + eighth + up + end + + + + + + + F + 1 + 4 + + 10080 + quarter + + single + + + + + + F + 1 + 4 + + 10080 + quarter + + single + + + + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + C + 1 + 5 + + 5040 + eighth + + single + + + + + + C + 1 + 5 + + 5040 + eighth + + single + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + F + 1 + 4 + + 10080 + quarter + + + + + + + C + 1 + 5 + + 5040 + eighth + + single + + + + + + C + 1 + 5 + + 5040 + eighth + + + + + + + B + -1 + 4 + + 5040 + eighth + flat + + single + + + + + + 5040 + eighth + + + + C + 1 + 5 + + 5040 + eighth + + single + + + + + + C + 1 + 5 + + 5040 + eighth + + + + + + + B + -1 + 4 + + 5040 + eighth + flat + + single + + + + + + 5040 + eighth + + + + + + + F + 1 + 4 + + 10080 + quarter + + single + + + + + + F + 1 + 4 + + 10080 + quarter + + single + J + + + + + F + 1 + 4 + + 10080 + quarter + + single + + + + + + 10080 + quarter + + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + F + 1 + 4 + + 10080 + quarter + + single + + + + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + C + 1 + 5 + + 20160 + half + + single + + + + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + C + 1 + 5 + + 10080 + quarter + + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + F + 1 + 4 + + 10080 + quarter + + single + + + + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + 10080 + quarter + + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + F + 1 + 4 + + 10080 + quarter + + single + + + + + + + + + C + 1 + 5 + + 5040 + eighth + down + begin + + + + + + + C + 1 + 5 + + 5040 + eighth + down + end + + single + + + + + + C + 1 + 5 + + 5040 + eighth + down + begin + + single + + + + + + E + -1 + 5 + + 5040 + eighth + flat + down + end + + single + + + + + + C + 1 + 5 + + 10080 + quarter + + single + + + + + + 10080 + quarter + + + + + + + F + 1 + 5 + + 10080 + quarter + + single + + + + + + C + 1 + 5 + + 10080 + quarter + + + + + + + G + 1 + 5 + + 10080 + quarter + + single + + + + + + C + 1 + 5 + + 10080 + quarter + + + + + + + B + -1 + 4 + + 10080 + quarter + flat + + single + + + + + + G + 1 + 4 + + 10080 + quarter + + single + + + + + + F + 1 + 4 + + 10080 + quarter + + + + + + + 10080 + quarter + + + light-light + + + + + + + 40320 + + + + \ No newline at end of file diff --git a/src/models/__init__.py b/src/models/__init__.py index a135cf6..d92c010 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -1,4 +1,9 @@ from .db import db from .user import User from .score import Score -from .result import Result \ No newline at end of file +from .result import Result +from .resultScore_save import ResultScoreSave +from .uploadScore_save import UploadScoreSave +from .transform import TransformTranspose +from .transform import TransformLyrics +from .transform import TransformMelody \ No newline at end of file diff --git a/src/routes/auth.py b/src/routes/auth.py index fc8bf88..9edcfcf 100644 --- a/src/routes/auth.py +++ b/src/routes/auth.py @@ -1,9 +1,9 @@ import os import requests from flask import Blueprint, redirect, request, jsonify -from src.config import Config -from src.services.auth_service import handle_kakao_login, refresh_access_token from flasgger import swag_from +from src.config import Config +from src.services.auth_service import handle_kakao_login, refresh_access_token auth_bp = Blueprint('auth', __name__, url_prefix='/auth') @@ -79,7 +79,9 @@ def kakao_callback(): 'type': 'object', 'properties': { 'access_token': {'type': 'string'}, - 'refresh_token': {'type': 'string'} + 'refresh_token': {'type': 'string'}, + 'user_id': {'type': 'integer'}, + 'nickname': {'type': 'string'} } } }, @@ -88,23 +90,14 @@ def kakao_callback(): } } }) - def kakao_token(): try: - # 요청 내용 디버깅 로그 - print("🔥 /auth/kakao/token 요청 도착") - print("🔥 REQUEST HEADERS:", dict(request.headers)) - print("🔥 RAW BODY:", request.get_data(as_text=True)) - - # JSON이든 form이든 유연하게 처리 data = request.get_json(silent=True) or request.form or request.values code = data.get('code') if data else None if not code: - print("❌ No code provided") return jsonify({'error': 'No code provided'}), 400 - # 카카오 토큰 요청 token_url = "https://kauth.kakao.com/oauth/token" token_data = { 'grant_type': 'authorization_code', @@ -113,15 +106,11 @@ def kakao_token(): 'code': code, } token_response = requests.post(token_url, data=token_data) - print("🔐 Kakao token response:", token_response.status_code, token_response.text) - token_json = token_response.json() access_token = token_json.get('access_token') if not access_token: - print("❌ Failed to get access token") return jsonify({'error': 'Failed to get Kakao access token'}), 400 - # 유저 정보 요청 user_info_url = "https://kapi.kakao.com/v2/user/me" headers = {"Authorization": f"Bearer {access_token}"} user_info_response = requests.get(user_info_url, headers=headers) @@ -135,15 +124,11 @@ def kakao_token(): return jsonify(result), 200 except Exception as e: - print("❌ Unexpected error in kakao_token:", str(e)) return jsonify({'error': 'Internal server error', 'message': str(e)}), 500 - - @auth_bp.route('/refresh', methods=['POST']) def refresh(): - """ JWT 리프레시 토큰을 이용해 액세스 토큰 재발급 --- @@ -155,6 +140,8 @@ def refresh(): application/json: schema: type: object + required: + - refresh_token properties: refresh_token: type: string @@ -162,15 +149,22 @@ def refresh(): responses: 200: description: 액세스 토큰 재발급 성공 - schema: - type: object - properties: - access_token: - type: string + content: + application/json: + schema: + type: object + properties: + access_token: + type: string + example: "new.access.token" + refresh_token: + type: string + example: "original.refresh.token" + 400: + description: 리프레시 토큰 누락 401: description: 토큰 만료 또는 유효하지 않음 """ - data = request.get_json() refresh_token = data.get('refresh_token') @@ -181,7 +175,11 @@ def refresh(): if error: return jsonify({"error": error}), 401 - return jsonify({"access_token": new_access_token}), 200 + return jsonify({ + "access_token": new_access_token, + "refresh_token": refresh_token + }), 200 + @auth_bp.route("/test-token", methods=["POST"]) def issue_test_token(): @@ -191,28 +189,36 @@ def issue_test_token(): tags: - auth summary: 테스트용 JWT 토큰 발급 - description: 테스트용 유저 정보를 기반으로 accessToken, refreshToken을 발급합니다. + description: 테스트용 유저 정보를 기반으로 access_token, refresh_token을 자동 발급합니다. responses: 200: description: 토큰 발급 성공 - schema: - type: object - properties: - access_token: - type: string - description: "Access Token" - refresh_token: - type: string - description: "Refresh Token" + content: + application/json: + schema: + type: object + properties: + access_token: + type: string + description: "Access Token" + refresh_token: + type: string + description: "Refresh Token" + user_id: + type: integer + example: 1 + nickname: + type: string + example: "테스트유저" """ - # 테스트용 고정 유저 정보 kakao_id = "test_kakao_12345" nickname = "테스트유저" profile_image = "" result = handle_kakao_login(kakao_id, nickname, profile_image) return jsonify(result), 200 - + + @auth_bp.route("/logout", methods=["POST"]) def logout(): """ @@ -225,11 +231,13 @@ def logout(): responses: 200: description: 로그아웃 성공 - schema: - type: object - properties: - message: - type: string - example: "로그아웃 완료" + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "로그아웃 완료" """ return jsonify({"message": "로그아웃 완료"}), 200 diff --git a/src/routes/mypage_result_score.py b/src/routes/mypage_result_score.py index 0bb015d..1c147ff 100644 --- a/src/routes/mypage_result_score.py +++ b/src/routes/mypage_result_score.py @@ -5,95 +5,107 @@ delete_result_score ) from src.utils.jwt_util import decode_token +from flasgger import swag_from result_score_bp = Blueprint("result_score_bp", __name__, url_prefix="/mypage/result") -@result_score_bp.route("//save", methods=["POST"]) -def save_result(result_id): - """ - 변환 결과 저장 (키 변경, 가사, 멜로디) - --- - tags: - - Mypage - parameters: - - in: header - name: Authorization - required: true - description: Bearer 액세스 토큰 - schema: - type: string - - in: path - name: result_id - required: true - description: 저장할 결과 ID - schema: - type: integer - responses: - 201: - description: 변환 결과가 저장되었습니다 - 400: - description: 이미 저장된 결과입니다 - 401: - description: 인증 실패 - """ +# ✅ 공통 JWT 인증 함수 +def get_user_id_from_token(): auth_header = request.headers.get("Authorization", None) if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"message": "토큰이 필요합니다"}), 401 + return None, jsonify({"message": "토큰이 필요합니다"}), 401 token = auth_header.split(" ")[1] payload, error = decode_token(token) if error: - return jsonify({"message": error}), 401 + return None, jsonify({"message": error}), 401 + + return payload["user_id"], None, None + + +@result_score_bp.route("//save", methods=["POST"]) +@swag_from({ + 'tags': ['Mypage'], + 'summary': '변환 결과 저장 (키 변경, 가사, 멜로디)', + 'parameters': [ + { + 'name': 'Authorization', + 'in': 'header', + 'required': True, + 'description': 'Bearer 액세스 토큰', + 'schema': {'type': 'string'} + }, + { + 'name': 'result_id', + 'in': 'path', + 'required': True, + 'description': '저장할 결과 ID', + 'schema': {'type': 'integer'} + } + ], + 'responses': { + 201: {'description': '변환 결과가 저장되었습니다'}, + 400: {'description': '이미 저장된 결과입니다'}, + 401: {'description': '인증 실패'} + } +}) +def save_result(result_id): + user_id, error_response, status_code = get_user_id_from_token() + if error_response: + return error_response, status_code - user_id = payload["user_id"] if save_result_score(user_id, result_id): return jsonify({"message": "변환 결과가 저장되었습니다"}), 201 return jsonify({"message": "이미 저장된 결과입니다"}), 400 @result_score_bp.route("", methods=["GET"]) +@swag_from({ + 'tags': ['Mypage'], + 'summary': '저장한 변환 결과 목록 조회', + 'parameters': [ + { + 'name': 'Authorization', + 'in': 'header', + 'required': True, + 'description': 'Bearer 액세스 토큰', + 'schema': {'type': 'string'} + }, + { + 'name': 'type', + 'in': 'query', + 'required': False, + 'description': '결과 타입 필터 (transpose, lyrics, melody)', + 'schema': { + 'type': 'string', + 'enum': ['transpose', 'lyrics', 'melody'] + } + } + ], + 'responses': { + 200: { + 'description': '저장된 변환 결과 목록 반환', + 'content': { + 'application/json': { + 'example': [ + { + 'result_id': 1, + 'result_type': 'transpose', + 'saved_at': '2025-05-18T12:34:56' + } + ] + } + } + }, + 401: {'description': '인증 실패'} + } +}) def get_saved_results(): - """ - 저장한 변환 결과 목록 조회 - --- - tags: - - Mypage - parameters: - - in: header - name: Authorization - required: true - description: Bearer 액세스 토큰 - schema: - type: string - - in: query - name: type - required: false - description: 결과 타입 필터 (transpose, lyrics, melody) - schema: - type: string - responses: - 200: - description: 저장된 변환 결과 목록 반환 - content: - application/json: - example: - - result_id: 1 - result_type: "transpose" - saved_at: "2025-05-18T12:34:56" - 401: - description: 인증 실패 - """ - auth_header = request.headers.get("Authorization", None) - if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"message": "토큰이 필요합니다"}), 401 + user_id, error_response, status_code = get_user_id_from_token() + if error_response: + return error_response, status_code - token = auth_header.split(" ")[1] - payload, error = decode_token(token) - if error: - return jsonify({"message": error}), 401 - - user_id = payload["user_id"] result_type = request.args.get("type") saved = get_saved_result_scores(user_id, result_type) result = [ @@ -108,43 +120,36 @@ def get_saved_results(): @result_score_bp.route("/", methods=["DELETE"]) +@swag_from({ + 'tags': ['Mypage'], + 'summary': '저장한 변환 결과 삭제', + 'parameters': [ + { + 'name': 'Authorization', + 'in': 'header', + 'required': True, + 'description': 'Bearer 액세스 토큰', + 'schema': {'type': 'string'} + }, + { + 'name': 'result_id', + 'in': 'path', + 'required': True, + 'description': '삭제할 변환 결과 ID', + 'schema': {'type': 'integer'} + } + ], + 'responses': { + 200: {'description': '저장이 해제되었습니다'}, + 404: {'description': '저장 내역이 없습니다'}, + 401: {'description': '인증 실패'} + } +}) def delete_result(result_id): - """ - 저장한 변환 결과 삭제 - --- - tags: - - Mypage - parameters: - - in: header - name: Authorization - required: true - description: Bearer 액세스 토큰 - schema: - type: string - - in: path - name: result_id - required: true - description: 삭제할 변환 결과 ID - schema: - type: integer - responses: - 200: - description: 저장이 해제되었습니다 - 404: - description: 저장 내역이 없습니다 - 401: - description: 인증 실패 - """ - auth_header = request.headers.get("Authorization", None) - if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"message": "토큰이 필요합니다"}), 401 - - token = auth_header.split(" ")[1] - payload, error = decode_token(token) - if error: - return jsonify({"message": error}), 401 + user_id, error_response, status_code = get_user_id_from_token() + if error_response: + return error_response, status_code - user_id = payload["user_id"] if delete_result_score(user_id, result_id): return jsonify({"message": "저장이 해제되었습니다"}), 200 return jsonify({"message": "저장 내역이 없습니다"}), 404 diff --git a/src/routes/mypage_upload_score.py b/src/routes/mypage_upload_score.py index 546edd6..a69475b 100644 --- a/src/routes/mypage_upload_score.py +++ b/src/routes/mypage_upload_score.py @@ -5,88 +5,90 @@ delete_upload_score ) from src.utils.jwt_util import decode_token +from flasgger import swag_from upload_score_bp = Blueprint("upload_score_bp", __name__, url_prefix="/mypage/score") - -@upload_score_bp.route("//save", methods=["POST"]) -def save_score(score_id): - """ - 업로드한 악보 저장 - --- - tags: - - Mypage - parameters: - - in: header - name: Authorization - required: true - description: Bearer 액세스 토큰 - schema: - type: string - - in: path - name: score_id - required: true - description: 저장할 악보 ID - schema: - type: string - responses: - 201: - description: 업로드한 악보가 저장되었습니다 - 400: - description: 이미 저장된 악보입니다 - 401: - description: 인증 실패 - """ +# ✅ JWT 인증 공통 함수 +def get_user_id_from_token(): auth_header = request.headers.get("Authorization", None) if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"message": "토큰이 필요합니다"}), 401 - + return None, jsonify({"message": "토큰이 필요합니다"}), 401 token = auth_header.split(" ")[1] payload, error = decode_token(token) if error: - return jsonify({"message": error}), 401 + return None, jsonify({"message": error}), 401 + return payload["user_id"], None, None - user_id = payload["user_id"] +@upload_score_bp.route("//save", methods=["POST"]) +@swag_from({ + 'tags': ['Mypage'], + 'summary': '업로드한 악보 저장', + 'parameters': [ + { + 'name': 'Authorization', + 'in': 'header', + 'required': True, + 'description': 'Bearer 액세스 토큰', + 'schema': {'type': 'string'} + }, + { + 'name': 'score_id', + 'in': 'path', + 'required': True, + 'description': '저장할 악보 ID', + 'schema': {'type': 'string'} + } + ], + 'responses': { + 201: {'description': '업로드한 악보가 저장되었습니다'}, + 400: {'description': '이미 저장된 악보입니다'}, + 401: {'description': '인증 실패'} + } +}) +def save_score(score_id): + user_id, error_response, status_code = get_user_id_from_token() + if error_response: + return error_response, status_code if save_upload_score(user_id, score_id): return jsonify({"message": "업로드한 악보가 저장되었습니다"}), 201 return jsonify({"message": "이미 저장된 악보입니다"}), 400 - @upload_score_bp.route("", methods=["GET"]) +@swag_from({ + 'tags': ['Mypage'], + 'summary': '저장한 업로드 악보 목록 조회', + 'parameters': [ + { + 'name': 'Authorization', + 'in': 'header', + 'required': True, + 'description': 'Bearer 액세스 토큰', + 'schema': {'type': 'string'} + } + ], + 'responses': { + 200: { + 'description': '저장된 악보 목록 반환', + 'content': { + 'application/json': { + 'example': [ + { + 'score_id': "abc123", + 'saved_at': "2025-05-18T12:34:56" + } + ] + } + } + }, + 401: {'description': '인증 실패'} + } +}) def get_saved_scores(): - """ - 저장한 업로드 악보 목록 조회 - --- - tags: - - Mypage - parameters: - - in: header - name: Authorization - required: true - description: Bearer 액세스 토큰 - schema: - type: string - responses: - 200: - description: 저장된 악보 목록 반환 - content: - application/json: - example: - - score_id: "abc123" - saved_at: "2025-05-18T12:34:56" - 401: - description: 인증 실패 - """ - auth_header = request.headers.get("Authorization", None) - if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"message": "토큰이 필요합니다"}), 401 + user_id, error_response, status_code = get_user_id_from_token() + if error_response: + return error_response, status_code - token = auth_header.split(" ")[1] - payload, error = decode_token(token) - if error: - return jsonify({"message": error}), 401 - - user_id = payload["user_id"] saved = get_saved_upload_scores(user_id) result = [ { @@ -97,45 +99,37 @@ def get_saved_scores(): ] return jsonify(result), 200 - @upload_score_bp.route("/", methods=["DELETE"]) +@swag_from({ + 'tags': ['Mypage'], + 'summary': '저장한 업로드 악보 삭제', + 'parameters': [ + { + 'name': 'Authorization', + 'in': 'header', + 'required': True, + 'description': 'Bearer 액세스 토큰', + 'schema': {'type': 'string'} + }, + { + 'name': 'score_id', + 'in': 'path', + 'required': True, + 'description': '삭제할 악보 ID', + 'schema': {'type': 'string'} + } + ], + 'responses': { + 200: {'description': '저장이 해제되었습니다'}, + 404: {'description': '저장 내역이 없습니다'}, + 401: {'description': '인증 실패'} + } +}) def delete_score(score_id): - """ - 저장한 업로드 악보 삭제 - --- - tags: - - Mypage - parameters: - - in: header - name: Authorization - required: true - description: Bearer 액세스 토큰 - schema: - type: string - - in: path - name: score_id - required: true - description: 삭제할 악보 ID - schema: - type: string - responses: - 200: - description: 저장이 해제되었습니다 - 404: - description: 저장 내역이 없습니다 - 401: - description: 인증 실패 - """ - auth_header = request.headers.get("Authorization", None) - if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"message": "토큰이 필요합니다"}), 401 - - token = auth_header.split(" ")[1] - payload, error = decode_token(token) - if error: - return jsonify({"message": error}), 401 + user_id, error_response, status_code = get_user_id_from_token() + if error_response: + return error_response, status_code - user_id = payload["user_id"] if delete_upload_score(user_id, score_id): return jsonify({"message": "저장이 해제되었습니다"}), 200 return jsonify({"message": "저장 내역이 없습니다"}), 404 diff --git a/src/routes/transform.py b/src/routes/transform.py index de4bba2..1f17de6 100644 --- a/src/routes/transform.py +++ b/src/routes/transform.py @@ -2,7 +2,7 @@ from src.utils.transpose_helper import transpose_key from src.models.score import Score from src.models.result import Result -from src.services.transform_service import perform_transpose, extract_melody +from src.services.transform_service import perform_transpose, extract_melody, extract_lyrics transform_bp = Blueprint('transform', __name__) @@ -48,12 +48,6 @@ def transpose_preview_route(): example: "F → E (shift -1)" 400: description: 잘못된 요청 - schema: - type: object - properties: - error: - type: string - example: "Invalid key: Z" """ data = request.get_json() current_key = data.get('current_key') @@ -116,12 +110,6 @@ def transform_transpose_route(score_id): example: "Transpose completed successfully" 404: description: 악보 ID를 찾을 수 없음 - schema: - type: object - properties: - error: - type: string - example: "Score not found" """ score = Score.query.get(score_id) if not score: @@ -140,6 +128,7 @@ def transform_transpose_route(score_id): 'message': 'Transpose completed successfully' }), 201 + @transform_bp.route('/score//lyrics', methods=['POST']) def lyrics_extract_route(score_id): """ @@ -172,31 +161,23 @@ def lyrics_extract_route(score_id): example: "Lyrics extracted successfully" 404: description: 악보 ID를 찾을 수 없음 - schema: - type: object - properties: - error: - type: string - example: "Score not found" """ score = Score.query.get(score_id) if not score: return jsonify({'error': 'Score not found'}), 404 - from src.services.transform_service import extract_lyrics result_id = extract_lyrics(score) - result = Result.query.get(result_id) - text_path = result.text_path if result else f"convert_result/{result_id}.txt" + if not result: + return jsonify({"error": "Result not found"}), 500 return jsonify({ 'result_id': result_id, - 'text_path': text_path, + 'text_path': result.download_path, 'message': 'Lyrics extracted successfully' }), 200 - @transform_bp.route('/score//melody', methods=['POST']) def melody_extract_route(score_id): """ @@ -246,12 +227,6 @@ def melody_extract_route(score_id): example: "Melody extracted from measure 1 to 8" 404: description: 악보 ID를 찾을 수 없음 - schema: - type: object - properties: - error: - type: string - example: "Score not found" """ data = request.get_json() start = data.get('start_measure') @@ -262,12 +237,12 @@ def melody_extract_route(score_id): return jsonify({'error': 'Score not found'}), 404 result_id = extract_melody(score, start, end) - result = Result.query.get(result_id) - mp3_path = result.audio_path if result else f"convert_result/{result_id}.mp3" + if not result: + return jsonify({"error": "Result not found"}), 500 return jsonify({ 'result_id': result_id, - 'mp3_path': mp3_path, + 'mp3_path': result.audio_path, 'message': f'Melody extracted from measure {start} to {end}' }), 200 diff --git a/src/routes/user.py b/src/routes/user.py index 4170575..09f442c 100644 --- a/src/routes/user.py +++ b/src/routes/user.py @@ -4,9 +4,30 @@ from src.utils.jwt_util import decode_token from flasgger import swag_from import os +import uuid user_bp = Blueprint('user', __name__, url_prefix='/user') +UPLOAD_FOLDER = 'static/profile_images' +os.makedirs(UPLOAD_FOLDER, exist_ok=True) + +# ✅ 공통 인증 함수 +def get_current_user(): + auth_header = request.headers.get("Authorization") + if not auth_header or not auth_header.startswith("Bearer "): + return None, jsonify({"error": "Missing or invalid Authorization header"}), 401 + + token = auth_header.split(" ")[1] + payload, error = decode_token(token) + if error: + return None, jsonify({"error": error}), 401 + + user = User.query.get(payload["user_id"]) + if not user: + return None, jsonify({"error": "User not found"}), 404 + + return user, None, None + @user_bp.route('/me', methods=['GET']) @swag_from({ @@ -34,18 +55,9 @@ } }) def get_my_info(): - auth_header = request.headers.get("Authorization") - if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"error": "Missing or invalid Authorization header"}), 401 - - token = auth_header.split(" ")[1] - payload, error = decode_token(token) - if error: - return jsonify({"error": error}), 401 - - user = User.query.get(payload["user_id"]) - if not user: - return jsonify({"error": "User not found"}), 404 + user, error_resp, status = get_current_user() + if error_resp: + return error_resp, status return jsonify({ "user_id": user.id, @@ -91,21 +103,10 @@ def get_my_info(): 404: {'description': 'User not found'} } }) - - def update_my_info(): - auth_header = request.headers.get("Authorization") - if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"error": "Missing or invalid Authorization header"}), 401 - - token = auth_header.split(" ")[1] - payload, error = decode_token(token) - if error: - return jsonify({"error": error}), 401 - - user = User.query.get(payload["user_id"]) - if not user: - return jsonify({"error": "User not found"}), 404 + user, error_resp, status = get_current_user() + if error_resp: + return error_resp, status data = request.get_json() new_nickname = data.get("nickname") @@ -121,10 +122,6 @@ def update_my_info(): }), 200 - -UPLOAD_FOLDER = 'static/profile_images' -os.makedirs(UPLOAD_FOLDER, exist_ok=True) - @user_bp.route("/me/profile-image", methods=["PATCH"]) @swag_from({ 'summary' : '프로필 이미지 업로드/변경', @@ -156,20 +153,10 @@ def update_my_info(): 401: {'description': '유효하지 않은 토큰'} } }) - def update_profile_image(): - auth_header = request.headers.get("Authorization") - if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"error": "Missing or invalid Authorization header"}), 401 - - token = auth_header.split(" ")[1] - payload, error = decode_token(token) - if error: - return jsonify({"error": error}), 401 - - user = User.query.get(payload["user_id"]) - if not user: - return jsonify({"error": "User not found"}), 404 + user, error_resp, status = get_current_user() + if error_resp: + return error_resp, status if 'profile_image' not in request.files: return jsonify({"error": "No file part"}), 400 @@ -178,8 +165,8 @@ def update_profile_image(): if file.filename == '': return jsonify({"error": "No selected file"}), 400 - filename = secure_filename(file.filename) - file_path = os.path.join(UPLOAD_FOLDER, filename) + unique_filename = f"{uuid.uuid4().hex}_{secure_filename(file.filename)}" + file_path = os.path.join(UPLOAD_FOLDER, unique_filename) file.save(file_path) user.profile_image = file_path @@ -190,10 +177,11 @@ def update_profile_image(): "nickname": user.nickname, "profile_image": user.profile_image }), 200 - + + @user_bp.route("/me", methods=["DELETE"]) @swag_from({ - 'summary': '회원 탈회', + 'summary': '회원 탈퇴', 'tags': ['user'], 'security': [{'Bearer': []}], 'responses': { @@ -211,27 +199,15 @@ def update_profile_image(): 404: {'description': '사용자 없음'} } }) - - - def delete_my_account(): - auth_header = request.headers.get("Authorization") - if not auth_header or not auth_header.startswith("Bearer "): - return jsonify({"error": "Missing or invalid Authorization header"}), 401 - - token = auth_header.split(" ")[1] - payload, error = decode_token(token) - if error: - return jsonify({"error": error}), 401 - - user = User.query.get(payload["user_id"]) - if not user: - return jsonify({"error": "User not found"}), 404 + user, error_resp, status = get_current_user() + if error_resp: + return error_resp, status db.session.delete(user) db.session.commit() return jsonify({ - "user_id": payload["user_id"], + "user_id": user.id, "message": "User successfully deleted" - }), 200 \ No newline at end of file + }), 200 diff --git a/src/services/result_service.py b/src/services/result_service.py index b342cdb..4b731dd 100644 --- a/src/services/result_service.py +++ b/src/services/result_service.py @@ -9,16 +9,20 @@ def normalize_path(path): # 5-1: 키 변경된 악보 결과 이미지 def get_transpose_image(result_id): result = Result.query.get(result_id) + if not result or result.type != 'transpose': + raise FileNotFoundError("키 변경 악보 이미지 결과를 찾을 수 없습니다") image_path = normalize_path(result.image_path) - if not result or result.type != 'transpose' or not image_path or not os.path.exists(image_path): + if not image_path or not os.path.exists(image_path): raise FileNotFoundError("키 변경 악보 이미지 결과를 찾을 수 없습니다") return send_file(image_path, mimetype='image/png') # 키 변경된 PDF 파일 다운로드 def download_transpose_file(result_id): result = Result.query.get(result_id) + if not result or result.type != 'transpose': + raise FileNotFoundError("키 변경 악보 다운로드 파일을 찾을 수 없습니다") download_path = normalize_path(result.download_path) - if not result or result.type != 'transpose' or not download_path or not os.path.exists(download_path): + if not download_path or not os.path.exists(download_path): raise FileNotFoundError("키 변경 악보 다운로드 파일을 찾을 수 없습니다") return send_file(download_path, as_attachment=True) @@ -32,8 +36,10 @@ def get_lyrics_text(result_id): # 가사 다운로드 파일 def download_lyrics_file(result_id): result = Result.query.get(result_id) + if not result or result.type != 'lyrics': + raise FileNotFoundError("가사 다운로드 파일을 찾을 수 없습니다") download_path = normalize_path(result.download_path) - if not result or result.type != 'lyrics' or not download_path or not os.path.exists(download_path): + if not download_path or not os.path.exists(download_path): raise FileNotFoundError("가사 다운로드 파일을 찾을 수 없습니다") return send_file(download_path, as_attachment=True) @@ -47,7 +53,9 @@ def get_melody_meta_info(result_id): # 멜로디 오디오 MP3 def get_melody_audio(result_id): result = Result.query.get(result_id) + if not result or result.type != 'melody': + raise FileNotFoundError("멜로디 오디오 파일을 찾을 수 없습니다") audio_path = normalize_path(result.audio_path) - if not result or result.type != 'melody' or not audio_path or not os.path.exists(audio_path): + if not audio_path or not os.path.exists(audio_path): raise FileNotFoundError("멜로디 오디오 파일을 찾을 수 없습니다") return send_file(audio_path, mimetype='audio/mpeg', as_attachment=True) diff --git a/src/services/transform_service.py b/src/services/transform_service.py index d201d7f..acbd62f 100644 --- a/src/services/transform_service.py +++ b/src/services/transform_service.py @@ -7,10 +7,9 @@ from music21 import midi, stream, note from src.models.db import db from src.models.score import Score -from src.models.result import Result # ✅ 통합된 Result 모델 +from src.models.result import Result from ML.src.makexml.MakeScore import MakeScore -# ✅ OS별 실행 경로 설정 if platform.system() == "Windows": FFMPEG_CMD = r"C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin\ffmpeg.exe" TIMIDITY_CMD = "timidity" @@ -20,11 +19,7 @@ TIMIDITY_CMD = "timidity" MSCORE_CMD = os.path.join("squashfs-root", "mscore4portable") - def perform_transpose(score: Score, shift: int) -> int: - """ - 키 변경을 수행하고 결과 PDF를 생성해 Result 테이블에 저장 - """ image_path = os.path.join('uploaded_scores', score.original_filename) img = cv2.imread(image_path, cv2.IMREAD_COLOR) img_list = [img] @@ -41,7 +36,6 @@ def perform_transpose(score: Score, shift: int) -> int: MakeScore.score_to_xml(transposed_score, result_id) - print("[Transpose] 실행 명령어:", [MSCORE_CMD, xml_path, "-o", pdf_path]) subprocess.run([MSCORE_CMD, xml_path, "-o", pdf_path], check=True) result = Result( @@ -54,11 +48,7 @@ def perform_transpose(score: Score, shift: int) -> int: return result.id - def extract_melody(score: Score, start_measure: int, end_measure: int) -> int: - """ - 악보에서 특정 마디 범위의 멜로디를 추출하여 MP3 파일로 저장 후 Result 테이블에 저장 - """ image_path = os.path.join('uploaded_scores', score.original_filename) img = cv2.imread(image_path, cv2.IMREAD_COLOR) img_list = [img] @@ -98,11 +88,7 @@ def extract_melody(score: Score, start_measure: int, end_measure: int) -> int: return result.id - def extract_lyrics(score: Score) -> int: - """ - 악보에서 가사를 추출하여 텍스트로 저장하고 Result 테이블에 저장 - """ image_path = os.path.join('uploaded_scores', score.original_filename) img = cv2.imread(image_path, cv2.IMREAD_COLOR) if img is None: @@ -111,15 +97,14 @@ def extract_lyrics(score: Score) -> int: img_list = [img] score_obj = MakeScore.make_score(img_list) - # 가사 추출 lyrics = [] for el in score_obj.recurse(): if isinstance(el, note.Note) and el.lyric: lyrics.append(el.lyric.strip()) lyrics_text = "\n".join(filter(None, lyrics)).strip() - if not lyrics_text: - raise ValueError("추출된 가사가 없습니다") + # if not lyrics_text: + # raise ValueError("추출된 가사가 없습니다") # 예외 제거 result_id = str(uuid.uuid4()) convert_dir = 'convert_result' @@ -132,8 +117,8 @@ def extract_lyrics(score: Score) -> int: result = Result( score_id=score.id, type='lyrics', - text_path=text_path, # 다운로드용 - text_content=lyrics_text # ✅ API 조회용 + download_path=text_path, + text_content=lyrics_text ) db.session.add(result) db.session.commit() diff --git a/src/services/user_service.py b/src/services/user_service.py deleted file mode 100644 index e69de29..0000000