diff --git a/src/__init__.py b/src/__init__.py index 0e519f6..0ad1cc3 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -7,8 +7,8 @@ def create_app(): app = Flask(__name__) - # ✅ CORS 설정: withCredentials 대응 - CORS(app, origins="http://localhost:5173", supports_credentials=True) + # ✅ CORS 설정: withCredentials 대응 + 모든 라우트에 확실하게 적용 + CORS(app, resources={r"/*": {"origins": "http://localhost:5173"}}, supports_credentials=True) app.config.from_object(Config) @@ -42,11 +42,10 @@ def create_app(): # 블루프린트 등록 from src.routes.index import index_bp app.register_blueprint(index_bp) - - # TranScore 블루프린트 등록 + from src.routes.auth import auth_bp from src.routes.user import user_bp - from src.routes.score import score_bp + from src.routes.score import score_bp from src.routes.transform import transform_bp from src.routes.result import result_bp from src.routes.mypage_upload_score import upload_score_bp diff --git a/src/routes/auth.py b/src/routes/auth.py index 9edcfcf..dda5226 100644 --- a/src/routes/auth.py +++ b/src/routes/auth.py @@ -34,20 +34,20 @@ def kakao_callback(): } token_response = requests.post(token_url, data=token_data) token_json = token_response.json() - access_token = token_json.get('access_token') - if not access_token: + accessToken = token_json.get('access_token') + if not accessToken: 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}"} + headers = {"Authorization": f"Bearer {accessToken}"} user_info_response = requests.get(user_info_url, headers=headers) user_info = user_info_response.json() - kakao_id = user_info.get("id") + kakaoId = user_info.get("id") nickname = user_info.get("properties", {}).get("nickname", "") - profile_image = user_info.get("properties", {}).get("profile_image", "") + profileImage = user_info.get("properties", {}).get("profile_image", "") - result = handle_kakao_login(kakao_id, nickname, profile_image) + result = handle_kakao_login(kakaoId, nickname, profileImage) return jsonify(result), 200 @@ -78,9 +78,9 @@ def kakao_callback(): 'schema': { 'type': 'object', 'properties': { - 'access_token': {'type': 'string'}, - 'refresh_token': {'type': 'string'}, - 'user_id': {'type': 'integer'}, + 'accessToken': {'type': 'string'}, + 'refreshToken': {'type': 'string'}, + 'userId': {'type': 'integer'}, 'nickname': {'type': 'string'} } } @@ -107,20 +107,20 @@ def kakao_token(): } token_response = requests.post(token_url, data=token_data) token_json = token_response.json() - access_token = token_json.get('access_token') - if not access_token: + accessToken = token_json.get('access_token') + if not accessToken: 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}"} + headers = {"Authorization": f"Bearer {accessToken}"} user_info_response = requests.get(user_info_url, headers=headers) user_info = user_info_response.json() - kakao_id = user_info.get("id") + kakaoId = user_info.get("id") nickname = user_info.get("properties", {}).get("nickname", "") - profile_image = user_info.get("properties", {}).get("profile_image", "") + profileImage = user_info.get("properties", {}).get("profile_image", "") - result = handle_kakao_login(kakao_id, nickname, profile_image) + result = handle_kakao_login(kakaoId, nickname, profileImage) return jsonify(result), 200 except Exception as e: @@ -141,9 +141,9 @@ def refresh(): schema: type: object required: - - refresh_token + - refreshToken properties: - refresh_token: + refreshToken: type: string example: "abc.def.ghi" responses: @@ -154,10 +154,10 @@ def refresh(): schema: type: object properties: - access_token: + accessToken: type: string example: "new.access.token" - refresh_token: + refreshToken: type: string example: "original.refresh.token" 400: @@ -166,18 +166,18 @@ def refresh(): description: 토큰 만료 또는 유효하지 않음 """ data = request.get_json() - refresh_token = data.get('refresh_token') + refreshToken = data.get('refreshToken') - if not refresh_token: + if not refreshToken: return jsonify({"error": "No refresh token provided"}), 400 - new_access_token, error = refresh_access_token(refresh_token) + newAccessToken, error = refresh_access_token(refreshToken) if error: return jsonify({"error": error}), 401 return jsonify({ - "access_token": new_access_token, - "refresh_token": refresh_token + "accessToken": newAccessToken, + "refreshToken": refreshToken }), 200 @@ -189,7 +189,7 @@ def issue_test_token(): tags: - auth summary: 테스트용 JWT 토큰 발급 - description: 테스트용 유저 정보를 기반으로 access_token, refresh_token을 자동 발급합니다. + description: 테스트용 유저 정보를 기반으로 accessToken, refreshToken을 자동 발급합니다. responses: 200: description: 토큰 발급 성공 @@ -198,24 +198,24 @@ def issue_test_token(): schema: type: object properties: - access_token: + accessToken: type: string description: "Access Token" - refresh_token: + refreshToken: type: string description: "Refresh Token" - user_id: + userId: type: integer example: 1 nickname: type: string example: "테스트유저" """ - kakao_id = "test_kakao_12345" + kakaoId = "test_kakao_12345" nickname = "테스트유저" - profile_image = "" + profileImage = "" - result = handle_kakao_login(kakao_id, nickname, profile_image) + result = handle_kakao_login(kakaoId, nickname, profileImage) return jsonify(result), 200 @@ -227,7 +227,7 @@ def logout(): tags: - auth summary: 로그아웃 - description: 클라이언트가 저장한 access_token 및 refresh_token을 삭제하면 로그아웃이 완료됩니다. 서버에서는 별도 처리를 하지 않습니다. + description: 클라이언트가 저장한 accessToken 및 refreshToken을 삭제하면 로그아웃이 완료됩니다. 서버에서는 별도 처리를 하지 않습니다. responses: 200: description: 로그아웃 성공 diff --git a/src/routes/mypage_result_score.py b/src/routes/mypage_result_score.py index 1c147ff..d93baac 100644 --- a/src/routes/mypage_result_score.py +++ b/src/routes/mypage_result_score.py @@ -11,20 +11,20 @@ # ✅ 공통 JWT 인증 함수 -def get_user_id_from_token(): - auth_header = request.headers.get("Authorization", None) - if not auth_header or not auth_header.startswith("Bearer "): +def getUserIdFromToken(): + authHeader = request.headers.get("Authorization", None) + if not authHeader or not authHeader.startswith("Bearer "): return None, jsonify({"message": "토큰이 필요합니다"}), 401 - token = auth_header.split(" ")[1] + token = authHeader.split(" ")[1] payload, error = decode_token(token) if error: return None, jsonify({"message": error}), 401 - return payload["user_id"], None, None + return payload["userId"], None, None -@result_score_bp.route("//save", methods=["POST"]) +@result_score_bp.route("//save", methods=["POST"]) @swag_from({ 'tags': ['Mypage'], 'summary': '변환 결과 저장 (키 변경, 가사, 멜로디)', @@ -37,7 +37,7 @@ def get_user_id_from_token(): 'schema': {'type': 'string'} }, { - 'name': 'result_id', + 'name': 'resultId', 'in': 'path', 'required': True, 'description': '저장할 결과 ID', @@ -50,12 +50,12 @@ def get_user_id_from_token(): 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 +def saveResult(resultId): + userId, errorResponse, statusCode = getUserIdFromToken() + if errorResponse: + return errorResponse, statusCode - if save_result_score(user_id, result_id): + if save_result_score(userId, resultId): return jsonify({"message": "변환 결과가 저장되었습니다"}), 201 return jsonify({"message": "이미 저장된 결과입니다"}), 400 @@ -90,9 +90,9 @@ def save_result(result_id): 'application/json': { 'example': [ { - 'result_id': 1, - 'result_type': 'transpose', - 'saved_at': '2025-05-18T12:34:56' + 'resultId': 1, + 'resultType': 'transpose', + 'savedAt': '2025-05-18T12:34:56' } ] } @@ -101,25 +101,25 @@ def save_result(result_id): 401: {'description': '인증 실패'} } }) -def get_saved_results(): - user_id, error_response, status_code = get_user_id_from_token() - if error_response: - return error_response, status_code +def getSavedResults(): + userId, errorResponse, statusCode = getUserIdFromToken() + if errorResponse: + return errorResponse, statusCode - result_type = request.args.get("type") - saved = get_saved_result_scores(user_id, result_type) + resultType = request.args.get("type") + saved = get_saved_result_scores(userId, resultType) result = [ { - "result_id": s.result_id, - "result_type": s.result.type, - "saved_at": s.saved_at.isoformat() + "resultId": s.result_id, + "resultType": s.result.type, + "savedAt": s.saved_at.isoformat() } for s in saved ] return jsonify(result), 200 -@result_score_bp.route("/", methods=["DELETE"]) +@result_score_bp.route("/", methods=["DELETE"]) @swag_from({ 'tags': ['Mypage'], 'summary': '저장한 변환 결과 삭제', @@ -132,7 +132,7 @@ def get_saved_results(): 'schema': {'type': 'string'} }, { - 'name': 'result_id', + 'name': 'resultId', 'in': 'path', 'required': True, 'description': '삭제할 변환 결과 ID', @@ -145,11 +145,11 @@ def get_saved_results(): 401: {'description': '인증 실패'} } }) -def delete_result(result_id): - user_id, error_response, status_code = get_user_id_from_token() - if error_response: - return error_response, status_code +def deleteResult(resultId): + userId, errorResponse, statusCode = getUserIdFromToken() + if errorResponse: + return errorResponse, statusCode - if delete_result_score(user_id, result_id): + if delete_result_score(userId, resultId): 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 a69475b..7d08570 100644 --- a/src/routes/mypage_upload_score.py +++ b/src/routes/mypage_upload_score.py @@ -9,18 +9,20 @@ upload_score_bp = Blueprint("upload_score_bp", __name__, url_prefix="/mypage/score") + # ✅ JWT 인증 공통 함수 -def get_user_id_from_token(): - auth_header = request.headers.get("Authorization", None) - if not auth_header or not auth_header.startswith("Bearer "): +def getUserIdFromToken(): + authHeader = request.headers.get("Authorization", None) + if not authHeader or not authHeader.startswith("Bearer "): return None, jsonify({"message": "토큰이 필요합니다"}), 401 - token = auth_header.split(" ")[1] + token = authHeader.split(" ")[1] payload, error = decode_token(token) if error: return None, jsonify({"message": error}), 401 - return payload["user_id"], None, None + return payload["userId"], None, None + -@upload_score_bp.route("//save", methods=["POST"]) +@upload_score_bp.route("//save", methods=["POST"]) @swag_from({ 'tags': ['Mypage'], 'summary': '업로드한 악보 저장', @@ -33,7 +35,7 @@ def get_user_id_from_token(): 'schema': {'type': 'string'} }, { - 'name': 'score_id', + 'name': 'scoreId', 'in': 'path', 'required': True, 'description': '저장할 악보 ID', @@ -46,14 +48,15 @@ def get_user_id_from_token(): 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): +def saveScore(scoreId): + userId, errorResponse, statusCode = getUserIdFromToken() + if errorResponse: + return errorResponse, statusCode + if save_upload_score(userId, scoreId): return jsonify({"message": "업로드한 악보가 저장되었습니다"}), 201 return jsonify({"message": "이미 저장된 악보입니다"}), 400 + @upload_score_bp.route("", methods=["GET"]) @swag_from({ 'tags': ['Mypage'], @@ -74,8 +77,8 @@ def save_score(score_id): 'application/json': { 'example': [ { - 'score_id': "abc123", - 'saved_at': "2025-05-18T12:34:56" + 'scoreId': "abc123", + 'savedAt': "2025-05-18T12:34:56" } ] } @@ -84,22 +87,23 @@ def save_score(score_id): 401: {'description': '인증 실패'} } }) -def get_saved_scores(): - user_id, error_response, status_code = get_user_id_from_token() - if error_response: - return error_response, status_code +def getSavedScores(): + userId, errorResponse, statusCode = getUserIdFromToken() + if errorResponse: + return errorResponse, statusCode - saved = get_saved_upload_scores(user_id) + saved = get_saved_upload_scores(userId) result = [ { - "score_id": s.score_id, - "saved_at": s.saved_at.isoformat() + "scoreId": s.score_id, + "savedAt": s.saved_at.isoformat() } for s in saved ] return jsonify(result), 200 -@upload_score_bp.route("/", methods=["DELETE"]) + +@upload_score_bp.route("/", methods=["DELETE"]) @swag_from({ 'tags': ['Mypage'], 'summary': '저장한 업로드 악보 삭제', @@ -112,7 +116,7 @@ def get_saved_scores(): 'schema': {'type': 'string'} }, { - 'name': 'score_id', + 'name': 'scoreId', 'in': 'path', 'required': True, 'description': '삭제할 악보 ID', @@ -125,11 +129,11 @@ def get_saved_scores(): 401: {'description': '인증 실패'} } }) -def delete_score(score_id): - user_id, error_response, status_code = get_user_id_from_token() - if error_response: - return error_response, status_code +def deleteScore(scoreId): + userId, errorResponse, statusCode = getUserIdFromToken() + if errorResponse: + return errorResponse, statusCode - if delete_upload_score(user_id, score_id): + if delete_upload_score(userId, scoreId): return jsonify({"message": "저장이 해제되었습니다"}), 200 return jsonify({"message": "저장 내역이 없습니다"}), 404 diff --git a/src/routes/result.py b/src/routes/result.py index a6dbb3b..1c3852d 100644 --- a/src/routes/result.py +++ b/src/routes/result.py @@ -11,13 +11,14 @@ result_bp = Blueprint('result', __name__, url_prefix='/result') + # 5-1. 키 변경된 악보 결과 -@result_bp.route('/transpose//image', methods=['GET']) +@result_bp.route('/transpose//image', methods=['GET']) @swag_from({ 'tags': ['Result - Transpose'], 'parameters': [ { - 'name': 'result_id', + 'name': 'resultId', 'in': 'path', 'type': 'integer', 'required': True, @@ -29,18 +30,19 @@ 404: {'description': '결과 없음'} } }) -def transpose_image(result_id): +def transposeImage(resultId): try: - return get_transpose_image(result_id) + return get_transpose_image(resultId) except FileNotFoundError as e: return jsonify({'error': str(e)}), 404 -@result_bp.route('/transpose//download', methods=['GET']) + +@result_bp.route('/transpose//download', methods=['GET']) @swag_from({ 'tags': ['Result - Transpose'], 'parameters': [ { - 'name': 'result_id', + 'name': 'resultId', 'in': 'path', 'type': 'integer', 'required': True, @@ -52,19 +54,20 @@ def transpose_image(result_id): 404: {'description': '결과 없음'} } }) -def transpose_download(result_id): +def transposeDownload(resultId): try: - return download_transpose_file(result_id) + return download_transpose_file(resultId) except FileNotFoundError as e: return jsonify({'error': str(e)}), 404 + # 5-2. 가사 추출 결과 -@result_bp.route('/lyrics//text', methods=['GET']) +@result_bp.route('/lyrics//text', methods=['GET']) @swag_from({ 'tags': ['Result - Lyrics'], 'parameters': [ { - 'name': 'result_id', + 'name': 'resultId', 'in': 'path', 'type': 'integer', 'required': True, @@ -76,18 +79,19 @@ def transpose_download(result_id): 404: {'description': '결과 없음'} } }) -def lyrics_text(result_id): +def lyricsText(resultId): try: - return get_lyrics_text(result_id) + return get_lyrics_text(resultId) except FileNotFoundError as e: return jsonify({'error': str(e)}), 404 -@result_bp.route('/lyrics//download', methods=['GET']) + +@result_bp.route('/lyrics//download', methods=['GET']) @swag_from({ 'tags': ['Result - Lyrics'], 'parameters': [ { - 'name': 'result_id', + 'name': 'resultId', 'in': 'path', 'type': 'integer', 'required': True, @@ -99,19 +103,20 @@ def lyrics_text(result_id): 404: {'description': '결과 없음'} } }) -def lyrics_download(result_id): +def lyricsDownload(resultId): try: - return download_lyrics_file(result_id) + return download_lyrics_file(resultId) except FileNotFoundError as e: return jsonify({'error': str(e)}), 404 + # 5-3. 멜로디 추출 결과 -@result_bp.route('/melody//info', methods=['GET']) +@result_bp.route('/melody//info', methods=['GET']) @swag_from({ 'tags': ['Result - Melody'], 'parameters': [ { - 'name': 'result_id', + 'name': 'resultId', 'in': 'path', 'type': 'integer', 'required': True, @@ -123,18 +128,19 @@ def lyrics_download(result_id): 404: {'description': '결과 없음'} } }) -def melody_info(result_id): +def melodyInfo(resultId): try: - return get_melody_meta_info(result_id) + return get_melody_meta_info(resultId) except FileNotFoundError as e: return jsonify({'error': str(e)}), 404 -@result_bp.route('/melody//audio', methods=['GET']) + +@result_bp.route('/melody//audio', methods=['GET']) @swag_from({ 'tags': ['Result - Melody'], 'parameters': [ { - 'name': 'result_id', + 'name': 'resultId', 'in': 'path', 'type': 'integer', 'required': True, @@ -146,8 +152,8 @@ def melody_info(result_id): 404: {'description': '결과 없음'} } }) -def melody_audio(result_id): +def melodyAudio(resultId): try: - return get_melody_audio(result_id) + return get_melody_audio(resultId) except FileNotFoundError as e: return jsonify({'error': str(e)}), 404 diff --git a/src/routes/score.py b/src/routes/score.py index 04f55d5..77bd37a 100644 --- a/src/routes/score.py +++ b/src/routes/score.py @@ -80,15 +80,15 @@ def upload_score_route(): return jsonify({'error': str(e)}), 500 -@score_bp.route('/score/', methods=['GET']) -def get_score_route(score_id): +@score_bp.route('/score/', methods=['GET']) +def get_score_route(scoreId): """ 악보 정보 조회 API --- tags: - score parameters: - - name: score_id + - name: scoreId in: path type: integer required: true @@ -106,22 +106,22 @@ def get_score_route(score_id): 404: description: 악보를 찾을 수 없음 """ - result = get_score(score_id) + result = get_score(scoreId) if result: return jsonify(result), 200 else: return jsonify({'error': 'Score not found'}), 404 -@score_bp.route('/score/', methods=['DELETE']) -def delete_score_route(score_id): +@score_bp.route('/score/', methods=['DELETE']) +def delete_score_route(scoreId): """ 악보 삭제 API --- tags: - score parameters: - - name: score_id + - name: scoreId in: path type: integer required: true @@ -135,7 +135,7 @@ def delete_score_route(score_id): 404: description: 악보를 찾을 수 없음 """ - success = delete_score(score_id) + success = delete_score(scoreId) if success: return jsonify({'message': 'Score deleted'}), 200 else: diff --git a/src/routes/transform.py b/src/routes/transform.py index 1f17de6..fad63e7 100644 --- a/src/routes/transform.py +++ b/src/routes/transform.py @@ -6,8 +6,9 @@ transform_bp = Blueprint('transform', __name__) + @transform_bp.route('/transpose-preview', methods=['POST']) -def transpose_preview_route(): +def transposePreviewRoute(): """ 키 변경 미리보기 API --- @@ -23,7 +24,7 @@ def transpose_preview_route(): schema: type: object properties: - current_key: + currentKey: type: string example: "F" description: 현재 키 @@ -32,7 +33,7 @@ def transpose_preview_route(): example: -1 description: 변환할 반음 수 required: - - current_key + - currentKey - shift responses: 200: @@ -40,7 +41,7 @@ def transpose_preview_route(): schema: type: object properties: - transposed_key: + transposedKey: type: string example: "E" message: @@ -50,27 +51,26 @@ def transpose_preview_route(): description: 잘못된 요청 """ data = request.get_json() - current_key = data.get('current_key') + currentKey = data.get('currentKey') shift = data.get('shift') - if current_key is None or shift is None: - return jsonify({'error': 'current_key and shift are required'}), 400 + if currentKey is None or shift is None: + return jsonify({'error': 'currentKey and shift are required'}), 400 try: shift = int(shift) - transposed_key = transpose_key(current_key, shift) - + transposedKey = transpose_key(currentKey, shift) return jsonify({ - 'transposed_key': transposed_key, - 'message': f"{current_key.upper()} → {transposed_key} (shift {shift})" + 'transposedKey': transposedKey, + 'message': f"{currentKey.upper()} → {transposedKey} (shift {shift})" }), 200 except ValueError as e: return jsonify({'error': str(e)}), 400 -@transform_bp.route('/score//transpose', methods=['POST']) -def transform_transpose_route(score_id): +@transform_bp.route('/score//transpose', methods=['POST']) +def transformTransposeRoute(scoreId): """ 키 변경 수행 API --- @@ -79,7 +79,7 @@ def transform_transpose_route(score_id): summary: 업로드된 악보를 지정된 반음 수만큼 키 변경하고 결과 PDF를 생성합니다 parameters: - in: path - name: score_id + name: scoreId required: true schema: type: integer @@ -102,7 +102,7 @@ def transform_transpose_route(score_id): schema: type: object properties: - result_id: + resultId: type: integer example: 101 message: @@ -111,26 +111,25 @@ def transform_transpose_route(score_id): 404: description: 악보 ID를 찾을 수 없음 """ - score = Score.query.get(score_id) + score = Score.query.get(scoreId) if not score: return jsonify({'error': 'Score not found'}), 404 data = request.get_json() shift = data.get('shift') - if shift is None: return jsonify({'error': 'shift is required'}), 400 - result_id = perform_transpose(score, int(shift)) + resultId = perform_transpose(score, int(shift)) return jsonify({ - 'result_id': result_id, + 'resultId': resultId, 'message': 'Transpose completed successfully' }), 201 -@transform_bp.route('/score//lyrics', methods=['POST']) -def lyrics_extract_route(score_id): +@transform_bp.route('/score//lyrics', methods=['POST']) +def lyricsExtractRoute(scoreId): """ 가사 추출 API --- @@ -139,7 +138,7 @@ def lyrics_extract_route(score_id): summary: 업로드된 악보에서 가사를 추출하여 텍스트 파일로 저장하고 결과 ID를 반환합니다 parameters: - in: path - name: score_id + name: scoreId required: true schema: type: integer @@ -150,10 +149,10 @@ def lyrics_extract_route(score_id): schema: type: object properties: - result_id: + resultId: type: integer example: 301 - text_path: + textPath: type: string example: "convert_result/301.txt" message: @@ -162,24 +161,24 @@ def lyrics_extract_route(score_id): 404: description: 악보 ID를 찾을 수 없음 """ - score = Score.query.get(score_id) + score = Score.query.get(scoreId) if not score: return jsonify({'error': 'Score not found'}), 404 - result_id = extract_lyrics(score) - result = Result.query.get(result_id) + resultId = extract_lyrics(score) + result = Result.query.get(resultId) if not result: return jsonify({"error": "Result not found"}), 500 return jsonify({ - 'result_id': result_id, - 'text_path': result.download_path, + 'resultId': resultId, + 'textPath': result.download_path, 'message': 'Lyrics extracted successfully' }), 200 -@transform_bp.route('/score//melody', methods=['POST']) -def melody_extract_route(score_id): +@transform_bp.route('/score//melody', methods=['POST']) +def melodyExtractRoute(scoreId): """ 멜로디 추출 API --- @@ -188,7 +187,7 @@ def melody_extract_route(score_id): summary: 업로드된 악보에서 특정 마디 범위의 멜로디를 추출하고 MP3 파일을 생성합니다 parameters: - in: path - name: score_id + name: scoreId required: true schema: type: integer @@ -199,27 +198,27 @@ def melody_extract_route(score_id): schema: type: object properties: - start_measure: + startMeasure: type: integer example: 1 description: 시작 마디 - end_measure: + endMeasure: type: integer example: 8 description: 종료 마디 required: - - start_measure - - end_measure + - startMeasure + - endMeasure responses: 200: description: 멜로디 추출 완료 schema: type: object properties: - result_id: + resultId: type: integer example: 205 - mp3_path: + mp3Path: type: string example: "convert_result/205.mp3" message: @@ -229,20 +228,20 @@ def melody_extract_route(score_id): description: 악보 ID를 찾을 수 없음 """ data = request.get_json() - start = data.get('start_measure') - end = data.get('end_measure') + start = data.get('startMeasure') + end = data.get('endMeasure') - score = Score.query.get(score_id) + score = Score.query.get(scoreId) if not score: return jsonify({'error': 'Score not found'}), 404 - result_id = extract_melody(score, start, end) - result = Result.query.get(result_id) + resultId = extract_melody(score, start, end) + result = Result.query.get(resultId) if not result: return jsonify({"error": "Result not found"}), 500 return jsonify({ - 'result_id': result_id, - 'mp3_path': result.audio_path, + 'resultId': resultId, + 'mp3Path': 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 09f442c..a6fc42d 100644 --- a/src/routes/user.py +++ b/src/routes/user.py @@ -12,17 +12,17 @@ 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 "): +def getCurrentUser(): + authHeader = request.headers.get("Authorization") + if not authHeader or not authHeader.startswith("Bearer "): return None, jsonify({"error": "Missing or invalid Authorization header"}), 401 - token = auth_header.split(" ")[1] + token = authHeader.split(" ")[1] payload, error = decode_token(token) if error: return None, jsonify({"error": error}), 401 - user = User.query.get(payload["user_id"]) + user = User.query.get(payload["userId"]) if not user: return None, jsonify({"error": "User not found"}), 404 @@ -31,7 +31,7 @@ def get_current_user(): @user_bp.route('/me', methods=['GET']) @swag_from({ - 'summary' : '내 정보 조회', + 'summary': '내 정보 조회', 'tags': ['user'], 'security': [{'Bearer': []}], 'responses': { @@ -40,29 +40,25 @@ def get_current_user(): 'schema': { 'type': 'object', 'properties': { - 'user_id': {'type': 'integer'}, + 'userId': {'type': 'integer'}, 'nickname': {'type': 'string'}, - 'profile_image': {'type': 'string'} + 'profileImage': {'type': 'string'} } } }, - 401: { - 'description': '유효하지 않은 토큰' - }, - 404: { - 'description': 'User not found' - } + 401: {'description': '유효하지 않은 토큰'}, + 404: {'description': 'User not found'} } }) -def get_my_info(): - user, error_resp, status = get_current_user() - if error_resp: - return error_resp, status +def getMyInfo(): + user, errorResp, status = getCurrentUser() + if errorResp: + return errorResp, status return jsonify({ - "user_id": user.id, + "userId": user.id, "nickname": user.nickname, - "profile_image": user.profile_image + "profileImage": user.profile_image }), 200 @@ -93,7 +89,7 @@ def get_my_info(): 'schema': { 'type': 'object', 'properties': { - 'user_id': {'type': 'integer'}, + 'userId': {'type': 'integer'}, 'nickname': {'type': 'string'} } } @@ -103,33 +99,33 @@ def get_my_info(): 404: {'description': 'User not found'} } }) -def update_my_info(): - user, error_resp, status = get_current_user() - if error_resp: - return error_resp, status +def updateMyInfo(): + user, errorResp, status = getCurrentUser() + if errorResp: + return errorResp, status data = request.get_json() - new_nickname = data.get("nickname") - if not new_nickname: + newNickname = data.get("nickname") + if not newNickname: return jsonify({"error": "Nickname is required"}), 400 - user.nickname = new_nickname + user.nickname = newNickname db.session.commit() return jsonify({ - "user_id": user.id, + "userId": user.id, "nickname": user.nickname }), 200 @user_bp.route("/me/profile-image", methods=["PATCH"]) @swag_from({ - 'summary' : '프로필 이미지 업로드/변경', + 'summary': '프로필 이미지 업로드/변경', 'tags': ['user'], 'consumes': ['multipart/form-data'], 'parameters': [ { - 'name': 'profile_image', + 'name': 'profileImage', 'in': 'formData', 'type': 'file', 'required': True, @@ -143,9 +139,9 @@ def update_my_info(): 'schema': { 'type': 'object', 'properties': { - 'user_id': {'type': 'integer'}, + 'userId': {'type': 'integer'}, 'nickname': {'type': 'string'}, - 'profile_image': {'type': 'string'} + 'profileImage': {'type': 'string'} } } }, @@ -153,29 +149,29 @@ def update_my_info(): 401: {'description': '유효하지 않은 토큰'} } }) -def update_profile_image(): - user, error_resp, status = get_current_user() - if error_resp: - return error_resp, status +def updateProfileImage(): + user, errorResp, status = getCurrentUser() + if errorResp: + return errorResp, status - if 'profile_image' not in request.files: + if 'profileImage' not in request.files: return jsonify({"error": "No file part"}), 400 - file = request.files['profile_image'] + file = request.files['profileImage'] if file.filename == '': return jsonify({"error": "No selected file"}), 400 - unique_filename = f"{uuid.uuid4().hex}_{secure_filename(file.filename)}" - file_path = os.path.join(UPLOAD_FOLDER, unique_filename) - file.save(file_path) + uniqueFilename = f"{uuid.uuid4().hex}_{secure_filename(file.filename)}" + filePath = os.path.join(UPLOAD_FOLDER, uniqueFilename) + file.save(filePath) - user.profile_image = file_path + user.profile_image = filePath db.session.commit() return jsonify({ - "user_id": user.id, + "userId": user.id, "nickname": user.nickname, - "profile_image": user.profile_image + "profileImage": user.profile_image }), 200 @@ -190,7 +186,7 @@ def update_profile_image(): 'schema': { 'type': 'object', 'properties': { - 'user_id': {'type': 'integer'}, + 'userId': {'type': 'integer'}, 'message': {'type': 'string'} } } @@ -199,15 +195,15 @@ def update_profile_image(): 404: {'description': '사용자 없음'} } }) -def delete_my_account(): - user, error_resp, status = get_current_user() - if error_resp: - return error_resp, status +def deleteMyAccount(): + user, errorResp, status = getCurrentUser() + if errorResp: + return errorResp, status db.session.delete(user) db.session.commit() return jsonify({ - "user_id": user.id, + "userId": user.id, "message": "User successfully deleted" }), 200 diff --git a/src/services/auth_service.py b/src/services/auth_service.py index d8f50fd..933d187 100644 --- a/src/services/auth_service.py +++ b/src/services/auth_service.py @@ -1,38 +1,38 @@ from src.utils.jwt_util import create_access_token, create_refresh_token, decode_token from src.models import db, User -def handle_kakao_login(kakao_id, nickname, profile_image_url): +def handle_kakao_login(kakaoId, nickname, profileImageUrl): # DB 컬럼에 맞춰 social_id로 조회 - user = User.query.filter_by(social_id=kakao_id).first() + user = User.query.filter_by(social_id=kakaoId).first() if not user: user = User( - social_id=kakao_id, # 컬럼명 맞게 수정 + social_id=kakaoId, nickname=nickname, - profile_image=profile_image_url # 모델 정의에 맞게 이름 일치시킴 + profile_image=profileImageUrl ) db.session.add(user) db.session.commit() - access_token = create_access_token(user.id) - refresh_token = create_refresh_token(user.id) + accessToken = create_access_token(user.id) + refreshToken = create_refresh_token(user.id) return { - "access_token": access_token, - "refresh_token": refresh_token, - "user_id": user.id, + "accessToken": accessToken, + "refreshToken": refreshToken, + "userId": user.id, "nickname": user.nickname } -def refresh_access_token(refresh_token): - payload = decode_token(refresh_token) +def refresh_access_token(refreshToken): + payload = decode_token(refreshToken) if not payload: return None, "Invalid or expired refresh token" - user_id = payload.get("user_id") - user = User.query.get(user_id) + userId = payload.get("userId") + user = User.query.get(userId) if not user: return None, "User not found" - new_access_token = create_access_token(user.id) - return new_access_token, None + newAccessToken = create_access_token(user.id) + return newAccessToken, None diff --git a/src/services/mypage_result_score.py b/src/services/mypage_result_score.py index b45c024..6061f86 100644 --- a/src/services/mypage_result_score.py +++ b/src/services/mypage_result_score.py @@ -3,26 +3,26 @@ from src.models.result import Result from src.models.user import User -def save_result_score(user_id, result_id): - exists = ResultScoreSave.query.filter_by(user_id=user_id, result_id=result_id).first() +def saveResultScore(userId, resultId): + exists = ResultScoreSave.query.filter_by(user_id=userId, result_id=resultId).first() if exists: return False # 이미 저장됨 - save = ResultScoreSave(user_id=user_id, result_id=result_id) + save = ResultScoreSave(user_id=userId, result_id=resultId) db.session.add(save) db.session.commit() return True -def get_saved_result_scores(user_id, result_type=None): - query = ResultScoreSave.query.join(Result).filter(ResultScoreSave.user_id == user_id) - if result_type: - query = query.filter(Result.type == result_type) +def getSavedResultScores(userId, resultType=None): + query = ResultScoreSave.query.join(Result).filter(ResultScoreSave.user_id == userId) + if resultType: + query = query.filter(Result.type == resultType) return query.all() -def delete_result_score(user_id, result_id): - save = ResultScoreSave.query.filter_by(user_id=user_id, result_id=result_id).first() +def deleteResultScore(userId, resultId): + save = ResultScoreSave.query.filter_by(user_id=userId, result_id=resultId).first() if not save: return False db.session.delete(save) diff --git a/src/services/mypage_upload_score.py b/src/services/mypage_upload_score.py index 902228b..35ecdec 100644 --- a/src/services/mypage_upload_score.py +++ b/src/services/mypage_upload_score.py @@ -3,23 +3,23 @@ from src.models.score import Score from src.models.user import User -def save_upload_score(user_id, score_id): - exists = UploadScoreSave.query.filter_by(user_id=user_id, score_id=score_id).first() +def saveUploadScore(userId, scoreId): + exists = UploadScoreSave.query.filter_by(user_id=userId, score_id=scoreId).first() if exists: return False # 이미 저장됨 - save = UploadScoreSave(user_id=user_id, score_id=score_id) + save = UploadScoreSave(user_id=userId, score_id=scoreId) db.session.add(save) db.session.commit() return True -def get_saved_upload_scores(user_id): - return UploadScoreSave.query.filter_by(user_id=user_id).all() +def getSavedUploadScores(userId): + return UploadScoreSave.query.filter_by(user_id=userId).all() -def delete_upload_score(user_id, score_id): - save = UploadScoreSave.query.filter_by(user_id=user_id, score_id=score_id).first() +def deleteUploadScore(userId, scoreId): + save = UploadScoreSave.query.filter_by(user_id=userId, score_id=scoreId).first() if not save: return False db.session.delete(save) diff --git a/src/services/result_service.py b/src/services/result_service.py index 4b731dd..b4f8b05 100644 --- a/src/services/result_service.py +++ b/src/services/result_service.py @@ -3,59 +3,59 @@ from src.models.result import Result # 내부 경로 정리 함수 -def normalize_path(path): +def normalizePath(path): return os.path.normpath(path) if path else None # 5-1: 키 변경된 악보 결과 이미지 -def get_transpose_image(result_id): - result = Result.query.get(result_id) +def getTransposeImage(resultId): + result = Result.query.get(resultId) if not result or result.type != 'transpose': raise FileNotFoundError("키 변경 악보 이미지 결과를 찾을 수 없습니다") - image_path = normalize_path(result.image_path) - if not image_path or not os.path.exists(image_path): + imagePath = normalizePath(result.image_path) + if not imagePath or not os.path.exists(imagePath): raise FileNotFoundError("키 변경 악보 이미지 결과를 찾을 수 없습니다") - return send_file(image_path, mimetype='image/png') + return send_file(imagePath, mimetype='image/png') # 키 변경된 PDF 파일 다운로드 -def download_transpose_file(result_id): - result = Result.query.get(result_id) +def downloadTransposeFile(resultId): + result = Result.query.get(resultId) if not result or result.type != 'transpose': raise FileNotFoundError("키 변경 악보 다운로드 파일을 찾을 수 없습니다") - download_path = normalize_path(result.download_path) - if not download_path or not os.path.exists(download_path): + downloadPath = normalizePath(result.download_path) + if not downloadPath or not os.path.exists(downloadPath): raise FileNotFoundError("키 변경 악보 다운로드 파일을 찾을 수 없습니다") - return send_file(download_path, as_attachment=True) + return send_file(downloadPath, as_attachment=True) # 5-2: 가사 추출 결과 텍스트 -def get_lyrics_text(result_id): - result = Result.query.get(result_id) +def getLyricsText(resultId): + result = Result.query.get(resultId) if not result or result.type != 'lyrics' or not result.text_content: raise FileNotFoundError("가사 텍스트 결과를 찾을 수 없습니다") - return jsonify({"text": result.text_content}) + return jsonify({"textContent": result.text_content}) # 가사 다운로드 파일 -def download_lyrics_file(result_id): - result = Result.query.get(result_id) +def downloadLyricsFile(resultId): + result = Result.query.get(resultId) if not result or result.type != 'lyrics': raise FileNotFoundError("가사 다운로드 파일을 찾을 수 없습니다") - download_path = normalize_path(result.download_path) - if not download_path or not os.path.exists(download_path): + downloadPath = normalizePath(result.download_path) + if not downloadPath or not os.path.exists(downloadPath): raise FileNotFoundError("가사 다운로드 파일을 찾을 수 없습니다") - return send_file(download_path, as_attachment=True) + return send_file(downloadPath, as_attachment=True) # 5-3: 멜로디 메타 정보 -def get_melody_meta_info(result_id): - result = Result.query.get(result_id) +def getMelodyMetaInfo(resultId): + result = Result.query.get(resultId) if not result or result.type != 'melody' or not result.meta_info: raise FileNotFoundError("멜로디 메타 정보가 없습니다") - return jsonify({"meta_info": result.meta_info}) + return jsonify({"metaInfo": result.meta_info}) # 멜로디 오디오 MP3 -def get_melody_audio(result_id): - result = Result.query.get(result_id) +def getMelodyAudio(resultId): + result = Result.query.get(resultId) if not result or result.type != 'melody': raise FileNotFoundError("멜로디 오디오 파일을 찾을 수 없습니다") - audio_path = normalize_path(result.audio_path) - if not audio_path or not os.path.exists(audio_path): + audioPath = normalizePath(result.audio_path) + if not audioPath or not os.path.exists(audioPath): raise FileNotFoundError("멜로디 오디오 파일을 찾을 수 없습니다") - return send_file(audio_path, mimetype='audio/mpeg', as_attachment=True) + return send_file(audioPath, mimetype='audio/mpeg', as_attachment=True) diff --git a/src/services/score_service.py b/src/services/score_service.py index 958cae2..32e2290 100644 --- a/src/services/score_service.py +++ b/src/services/score_service.py @@ -1,29 +1,29 @@ from src.models.score import db, Score -def save_score_to_db(filename, xml_path, pdf_path): - new_score = Score( +def saveScoreToDb(filename, xmlPath, pdfPath): + newScore = Score( original_filename=filename, - xml_path=xml_path, - pdf_path=pdf_path + xml_path=xmlPath, + pdf_path=pdfPath ) - db.session.add(new_score) + db.session.add(newScore) db.session.commit() - return new_score.id # 새로 생성된 정수 ID를 반환 + return newScore.id # 새로 생성된 정수 ID를 반환 -def get_score(score_id): - score = Score.query.get(score_id) +def getScore(scoreId): + score = Score.query.get(scoreId) if score: return { - 'score_id': score.id, - 'original_filename': score.original_filename, - 'xml_path': score.xml_path, - 'pdf_path': score.pdf_path, - 'created_at': score.created_at.isoformat() + 'scoreId': score.id, + 'originalFilename': score.original_filename, + 'xmlPath': score.xml_path, + 'pdfPath': score.pdf_path, + 'createdAt': score.created_at.isoformat() } return None -def delete_score(score_id): - score = Score.query.get(score_id) +def deleteScore(scoreId): + score = Score.query.get(scoreId) if score: db.session.delete(score) db.session.commit() diff --git a/src/services/transform_service.py b/src/services/transform_service.py index acbd62f..f301d2a 100644 --- a/src/services/transform_service.py +++ b/src/services/transform_service.py @@ -11,114 +11,115 @@ from ML.src.makexml.MakeScore import MakeScore if platform.system() == "Windows": - FFMPEG_CMD = r"C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin\ffmpeg.exe" - TIMIDITY_CMD = "timidity" - MSCORE_CMD = r"C:\Program Files\MuseScore 4\bin\MuseScore4.exe" + ffmpegCmd = r"C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin\ffmpeg.exe" + timidityCmd = "timidity" + mscoreCmd = r"C:\Program Files\MuseScore 4\bin\MuseScore4.exe" else: - FFMPEG_CMD = "ffmpeg" - TIMIDITY_CMD = "timidity" - MSCORE_CMD = os.path.join("squashfs-root", "mscore4portable") + ffmpegCmd = "ffmpeg" + timidityCmd = "timidity" + mscoreCmd = os.path.join("squashfs-root", "mscore4portable") -def perform_transpose(score: Score, shift: int) -> int: - image_path = os.path.join('uploaded_scores', score.original_filename) - img = cv2.imread(image_path, cv2.IMREAD_COLOR) - img_list = [img] - score_obj = MakeScore.make_score(img_list) - transposed_score = MakeScore.change_key(score_obj, shift) +def performTranspose(score: Score, shift: int) -> int: + imagePath = os.path.join('uploaded_scores', score.original_filename) + img = cv2.imread(imagePath, cv2.IMREAD_COLOR) + imgList = [img] - result_id = str(uuid.uuid4()) - convert_dir = 'convert_result' - os.makedirs(convert_dir, exist_ok=True) + scoreObj = MakeScore.make_score(imgList) + transposedScore = MakeScore.change_key(scoreObj, shift) - xml_path = os.path.join(convert_dir, f"{result_id}.xml") - pdf_path = os.path.join(convert_dir, f"{result_id}.pdf") + resultId = str(uuid.uuid4()) + convertDir = 'convert_result' + os.makedirs(convertDir, exist_ok=True) - MakeScore.score_to_xml(transposed_score, result_id) + xmlPath = os.path.join(convertDir, f"{resultId}.xml") + pdfPath = os.path.join(convertDir, f"{resultId}.pdf") - subprocess.run([MSCORE_CMD, xml_path, "-o", pdf_path], check=True) + MakeScore.score_to_xml(transposedScore, resultId) + subprocess.run([mscoreCmd, xmlPath, "-o", pdfPath], check=True) result = Result( score_id=score.id, type='transpose', - download_path=pdf_path + download_path=pdfPath ) db.session.add(result) db.session.commit() return result.id -def extract_melody(score: Score, start_measure: int, end_measure: int) -> int: - image_path = os.path.join('uploaded_scores', score.original_filename) - img = cv2.imread(image_path, cv2.IMREAD_COLOR) - img_list = [img] - score_obj = MakeScore.make_score(img_list) - - extracted_score = stream.Score() - for part in score_obj.parts: - part_extract = stream.Part() - for m in part.measures(start_measure, end_measure): - part_extract.append(m) - extracted_score.append(part_extract) - - result_id = str(uuid.uuid4()) - convert_dir = 'convert_result' - os.makedirs(convert_dir, exist_ok=True) - - midi_path = os.path.join(convert_dir, f"{result_id}.mid") - mp3_path = os.path.join(convert_dir, f"{result_id}.mp3") - wav_path = midi_path.replace('.mid', '.wav') - - mf = midi.translate.music21ObjectToMidiFile(extracted_score) - mf.open(midi_path, 'wb') + +def extractMelody(score: Score, startMeasure: int, endMeasure: int) -> int: + imagePath = os.path.join('uploaded_scores', score.original_filename) + img = cv2.imread(imagePath, cv2.IMREAD_COLOR) + imgList = [img] + + scoreObj = MakeScore.make_score(imgList) + + extractedScore = stream.Score() + for part in scoreObj.parts: + partExtract = stream.Part() + for m in part.measures(startMeasure, endMeasure): + partExtract.append(m) + extractedScore.append(partExtract) + + resultId = str(uuid.uuid4()) + convertDir = 'convert_result' + os.makedirs(convertDir, exist_ok=True) + + midiPath = os.path.join(convertDir, f"{resultId}.mid") + mp3Path = os.path.join(convertDir, f"{resultId}.mp3") + wavPath = midiPath.replace('.mid', '.wav') + + mf = midi.translate.music21ObjectToMidiFile(extractedScore) + mf.open(midiPath, 'wb') mf.write() mf.close() - subprocess.run([TIMIDITY_CMD, midi_path, "-Ow", "-o", wav_path], check=True) - subprocess.run([FFMPEG_CMD, "-i", wav_path, mp3_path], check=True) - os.remove(wav_path) + subprocess.run([timidityCmd, midiPath, "-Ow", "-o", wavPath], check=True) + subprocess.run([ffmpegCmd, "-i", wavPath, mp3Path], check=True) + os.remove(wavPath) result = Result( score_id=score.id, type='melody', - audio_path=mp3_path + audio_path=mp3Path ) db.session.add(result) db.session.commit() return result.id -def extract_lyrics(score: Score) -> int: - image_path = os.path.join('uploaded_scores', score.original_filename) - img = cv2.imread(image_path, cv2.IMREAD_COLOR) + +def extractLyrics(score: Score) -> int: + imagePath = os.path.join('uploaded_scores', score.original_filename) + img = cv2.imread(imagePath, cv2.IMREAD_COLOR) if img is None: raise RuntimeError("이미지를 불러올 수 없습니다") - img_list = [img] - score_obj = MakeScore.make_score(img_list) + imgList = [img] + scoreObj = MakeScore.make_score(imgList) lyrics = [] - for el in score_obj.recurse(): + for el in scoreObj.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("추출된 가사가 없습니다") # 예외 제거 + lyricsText = "\n".join(filter(None, lyrics)).strip() - result_id = str(uuid.uuid4()) - convert_dir = 'convert_result' - os.makedirs(convert_dir, exist_ok=True) + resultId = str(uuid.uuid4()) + convertDir = 'convert_result' + os.makedirs(convertDir, exist_ok=True) - text_path = os.path.join(convert_dir, f"{result_id}.txt") - with open(text_path, 'w', encoding='utf-8') as f: - f.write(lyrics_text) + textPath = os.path.join(convertDir, f"{resultId}.txt") + with open(textPath, 'w', encoding='utf-8') as f: + f.write(lyricsText) result = Result( score_id=score.id, type='lyrics', - download_path=text_path, - text_content=lyrics_text + download_path=textPath, + text_content=lyricsText ) db.session.add(result) db.session.commit() diff --git a/src/utils/jwt_util.py b/src/utils/jwt_util.py index 85c2a40..92103ce 100644 --- a/src/utils/jwt_util.py +++ b/src/utils/jwt_util.py @@ -6,18 +6,18 @@ JWT_ALGORITHM = "HS256" # 액세스 토큰 생성 (1시간 유효) -def create_access_token(user_id): +def create_access_token(userId): payload = { - "user_id": user_id, + "userId": userId, "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1) } token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) return token # 리프레시 토큰 생성 (7일 유효) -def create_refresh_token(user_id): +def create_refresh_token(userId): payload = { - "user_id": user_id, + "userId": userId, "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7) } token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) @@ -34,4 +34,3 @@ def decode_token(token): except jwt.InvalidTokenError: print("JWT 디코딩 실패: 유효하지 않은 토큰") return None, "Invalid token" - diff --git a/src/utils/transpose_helper.py b/src/utils/transpose_helper.py index f82a991..9848e78 100644 --- a/src/utils/transpose_helper.py +++ b/src/utils/transpose_helper.py @@ -1,11 +1,11 @@ KEYS = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] -def transpose_key(current_key: str, shift: int) -> str: - key_upper = current_key.upper().replace('B♭', 'A#').replace('E♭', 'D#') # 간단한 flat 처리 예시 +def transposeKey(currentKey: str, shift: int) -> str: + keyUpper = currentKey.upper().replace('B♭', 'A#').replace('E♭', 'D#') # 간단한 flat 처리 예시 - if key_upper not in KEYS: - raise ValueError(f"Invalid key: {current_key}") + if keyUpper not in KEYS: + raise ValueError(f"Invalid key: {currentKey}") - index = KEYS.index(key_upper) - new_index = (index + shift) % 12 - return KEYS[new_index] + index = KEYS.index(keyUpper) + newIndex = (index + shift) % 12 + return KEYS[newIndex]