From f8749babb8299a4b469c4a196333d2eb17e3e622 Mon Sep 17 00:00:00 2001 From: b1351b1 Date: Tue, 9 Dec 2025 20:55:35 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=F0=9F=90=9B=20Fix(be/progress):=20comple?= =?UTF-8?q?tion=5Frate=20100%=20=EC=B4=88=EA=B3=BC=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A7=84=ED=96=89=EB=A5=A0=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._add_unique_watched_seconds_to_progress.py | 38 ++++++++++++++++ app/models/progress.py | 3 +- app/services/enrollment_service.py | 45 +++++++++++++++---- 3 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 alembic/versions/a3a28d52a24f_add_unique_watched_seconds_to_progress.py diff --git a/alembic/versions/a3a28d52a24f_add_unique_watched_seconds_to_progress.py b/alembic/versions/a3a28d52a24f_add_unique_watched_seconds_to_progress.py new file mode 100644 index 0000000..211136f --- /dev/null +++ b/alembic/versions/a3a28d52a24f_add_unique_watched_seconds_to_progress.py @@ -0,0 +1,38 @@ +"""add unique_watched_seconds to progress table + +Revision ID: a3a28d52a24f +Revises: 81526e4abbc5 +Create Date: 2025-12-09 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a3a28d52a24f' +down_revision: Union[str, Sequence[str], None] = '81526e4abbc5' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # Add unique_watched_seconds column with default value 0 + op.add_column('progresses', sa.Column('unique_watched_seconds', sa.Integer(), nullable=False, server_default='0', comment='유니크 시청 시간 (초) - 진행률 계산용')) + + # Update existing records: set unique_watched_seconds to min(watched_seconds, lecture.duration_seconds) + # For simplicity, we'll initially set it equal to watched_seconds + # The application logic will handle proper calculation on next update + op.execute(""" + UPDATE progresses + SET unique_watched_seconds = watched_seconds + """) + + +def downgrade() -> None: + """Downgrade schema.""" + # Drop the unique_watched_seconds column + op.drop_column('progresses', 'unique_watched_seconds') diff --git a/app/models/progress.py b/app/models/progress.py index 8ea3e08..0c7c6a5 100644 --- a/app/models/progress.py +++ b/app/models/progress.py @@ -39,7 +39,8 @@ class Progress(Base): lecture_id = Column(Integer, ForeignKey("lectures.id", ondelete="CASCADE"), nullable=False, index=True, comment="강의 ID") # 진행 정보 - watched_seconds = Column(Integer, nullable=False, default=0, comment="총 시청 시간 (초)") + watched_seconds = Column(Integer, nullable=False, default=0, comment="총 시청 시간 (초) - 반복 재생 포함 누적") + unique_watched_seconds = Column(Integer, nullable=False, default=0, comment="유니크 시청 시간 (초) - 진행률 계산용") last_position = Column(Integer, nullable=False, default=0, comment="마지막 시청 위치 (초)") is_completed = Column(Boolean, nullable=False, default=False, comment="완료 여부") diff --git a/app/services/enrollment_service.py b/app/services/enrollment_service.py index fd2fc6f..8a351e0 100644 --- a/app/services/enrollment_service.py +++ b/app/services/enrollment_service.py @@ -79,12 +79,19 @@ async def create_progress( # 새로운 진행 기록 생성 now = get_current_utc_datetime() + # last_position을 duration_seconds 이하로 제한 + last_position = min(data.last_position, lecture.duration_seconds) if lecture.duration_seconds > 0 else data.last_position + + # unique_watched_seconds 계산 (진행률 계산용) + # last_position을 기준으로 유니크 시청 시간 계산 + unique_watched_seconds = last_position + # 시청률이 95% 이상이면 자동으로 완료 처리 is_completed = data.is_completed completed_at = None if lecture.duration_seconds > 0: - completion_rate = (data.watched_seconds / lecture.duration_seconds) * 100 + completion_rate = (unique_watched_seconds / lecture.duration_seconds) * 100 if completion_rate >= 95 or data.is_completed: is_completed = True completed_at = now @@ -95,7 +102,8 @@ async def create_progress( user_id=user_id, lecture_id=data.lecture_id, watched_seconds=data.watched_seconds, - last_position=data.last_position, + unique_watched_seconds=unique_watched_seconds, + last_position=last_position, is_completed=is_completed, last_watched_at=now, completed_at=completed_at @@ -135,14 +143,22 @@ async def update_progress( if not progress: raise ProgressNotFoundError('학습 진행 기록을 찾을 수 없습니다') + # last_position을 duration_seconds 이하로 제한 + last_position = min(data.last_position, progress.lecture.duration_seconds) if progress.lecture.duration_seconds > 0 else data.last_position + + # unique_watched_seconds 계산 (진행률 계산용) + # last_position을 기준으로 유니크 시청 시간 계산 + unique_watched_seconds = last_position + # 업데이트 progress.watched_seconds = data.watched_seconds - progress.last_position = data.last_position + progress.unique_watched_seconds = unique_watched_seconds + progress.last_position = last_position progress.last_watched_at = get_current_utc_datetime() # 시청률이 95% 이상이면 자동으로 완료 처리 if progress.lecture.duration_seconds > 0: - completion_rate = (data.watched_seconds / progress.lecture.duration_seconds) * 100 + completion_rate = (unique_watched_seconds / progress.lecture.duration_seconds) * 100 if completion_rate >= 95 or data.is_completed: progress.is_completed = True if progress.completed_at is None: @@ -216,12 +232,18 @@ async def get_course_progress( for lecture in sorted(chapter.lectures, key=lambda l: l.order_number): progress = progress_dict.get(lecture.id) + # watched_seconds는 누적 시청 시간 (분석용) watched_seconds = progress.watched_seconds if progress else 0 + # unique_watched_seconds는 유니크 시청 시간 (진행률 계산용) + unique_watched_seconds = progress.unique_watched_seconds if progress else 0 last_position = progress.last_position if progress else 0 is_completed = progress.is_completed if progress else False lecture_last_watched = progress.last_watched_at if progress else None - completion_rate = (watched_seconds / lecture.duration_seconds * 100) if lecture.duration_seconds > 0 else 0 + # unique_watched_seconds를 사용하여 completion_rate 계산 + completion_rate = (unique_watched_seconds / lecture.duration_seconds * 100) if lecture.duration_seconds > 0 else 0 + # 100% 초과 방지 + completion_rate = min(completion_rate, 100.0) lectures_data.append(LectureProgressSummary( lecture_id=lecture.id, @@ -234,7 +256,8 @@ async def get_course_progress( last_watched_at=lecture_last_watched )) - chapter_watched += watched_seconds + # 챕터 진행률 계산에는 unique_watched_seconds 사용 + chapter_watched += unique_watched_seconds chapter_duration += lecture.duration_seconds if is_completed: chapter_completed += 1 @@ -386,10 +409,14 @@ async def _build_progress_response( ) -> ProgressResponse: """학습 진행 응답 생성""" + # unique_watched_seconds를 사용하여 completion_rate 계산 completion_rate = ( - progress.watched_seconds / lecture.duration_seconds * 100 + progress.unique_watched_seconds / lecture.duration_seconds * 100 ) if lecture.duration_seconds > 0 else 0 + # 100% 초과 방지 + completion_rate = min(completion_rate, 100.0) + # 시청률이 95% 이상이면 완료로 간주 is_completed = progress.is_completed if lecture.duration_seconds > 0 and completion_rate >= 95: @@ -413,10 +440,10 @@ async def _get_total_watched_duration( user_id: int, course_id: int ) -> int: - """총 시청 시간 반환 (초)""" + """총 유니크 시청 시간 반환 (초) - 진행률 계산용""" result = await self.db.execute( - select(func.sum(Progress.watched_seconds)) + select(func.sum(Progress.unique_watched_seconds)) .join(Lecture, Progress.lecture_id == Lecture.id) .join(Chapter, Lecture.chapter_id == Chapter.id) .where( From 1403b96e1f7ba2f4cd09bde28a4dba9746a39395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B0=80=EB=9E=8C?= Date: Tue, 9 Dec 2025 21:42:37 +0900 Subject: [PATCH 02/11] Update alembic/versions/a3a28d52a24f_add_unique_watched_seconds_to_progress.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ...8d52a24f_add_unique_watched_seconds_to_progress.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/alembic/versions/a3a28d52a24f_add_unique_watched_seconds_to_progress.py b/alembic/versions/a3a28d52a24f_add_unique_watched_seconds_to_progress.py index 211136f..b0dd29c 100644 --- a/alembic/versions/a3a28d52a24f_add_unique_watched_seconds_to_progress.py +++ b/alembic/versions/a3a28d52a24f_add_unique_watched_seconds_to_progress.py @@ -23,12 +23,13 @@ def upgrade() -> None: # Add unique_watched_seconds column with default value 0 op.add_column('progresses', sa.Column('unique_watched_seconds', sa.Integer(), nullable=False, server_default='0', comment='유니크 시청 시간 (초) - 진행률 계산용')) - # Update existing records: set unique_watched_seconds to min(watched_seconds, lecture.duration_seconds) - # For simplicity, we'll initially set it equal to watched_seconds - # The application logic will handle proper calculation on next update + # Update existing records: set unique_watched_seconds to min(last_position, lecture.duration_seconds) op.execute(""" - UPDATE progresses - SET unique_watched_seconds = watched_seconds + UPDATE progresses p + SET unique_watched_seconds = LEAST( + p.last_position, + (SELECT l.duration_seconds FROM lectures l WHERE l.id = p.lecture_id) + ) """) From 469f050e97993821dc5492900c4620f54a8e7695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B0=80=EB=9E=8C?= Date: Tue, 9 Dec 2025 21:42:54 +0900 Subject: [PATCH 03/11] Update app/services/enrollment_service.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/services/enrollment_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/enrollment_service.py b/app/services/enrollment_service.py index 8a351e0..7bb8e1b 100644 --- a/app/services/enrollment_service.py +++ b/app/services/enrollment_service.py @@ -147,7 +147,7 @@ async def update_progress( last_position = min(data.last_position, progress.lecture.duration_seconds) if progress.lecture.duration_seconds > 0 else data.last_position # unique_watched_seconds 계산 (진행률 계산용) - # last_position을 기준으로 유니크 시청 시간 계산 + # last_position 값을 unique_watched_seconds로 사용 unique_watched_seconds = last_position # 업데이트 From 29acd2dac93a66fe328702db9610854f02037d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B0=80=EB=9E=8C?= Date: Tue, 9 Dec 2025 21:43:06 +0900 Subject: [PATCH 04/11] Update app/services/enrollment_service.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/services/enrollment_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/enrollment_service.py b/app/services/enrollment_service.py index 7bb8e1b..8471025 100644 --- a/app/services/enrollment_service.py +++ b/app/services/enrollment_service.py @@ -144,7 +144,7 @@ async def update_progress( raise ProgressNotFoundError('학습 진행 기록을 찾을 수 없습니다') # last_position을 duration_seconds 이하로 제한 - last_position = min(data.last_position, progress.lecture.duration_seconds) if progress.lecture.duration_seconds > 0 else data.last_position + last_position = min(data.last_position, progress.lecture.duration_seconds) if progress.lecture.duration_seconds > 0 else 0 # unique_watched_seconds 계산 (진행률 계산용) # last_position 값을 unique_watched_seconds로 사용 From 4ba3520e1e7c1f4fab05adda6207fc85ffd27ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B0=80=EB=9E=8C?= Date: Tue, 9 Dec 2025 21:43:15 +0900 Subject: [PATCH 05/11] Update app/services/enrollment_service.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/services/enrollment_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/enrollment_service.py b/app/services/enrollment_service.py index 8471025..0d0d3eb 100644 --- a/app/services/enrollment_service.py +++ b/app/services/enrollment_service.py @@ -148,7 +148,7 @@ async def update_progress( # unique_watched_seconds 계산 (진행률 계산용) # last_position 값을 unique_watched_seconds로 사용 - unique_watched_seconds = last_position + unique_watched_seconds = max(progress.unique_watched_seconds or 0, last_position) # 업데이트 progress.watched_seconds = data.watched_seconds From 6e8019fc3d5c006ed03cdbef75416d732da0c4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B0=80=EB=9E=8C?= Date: Tue, 9 Dec 2025 21:43:26 +0900 Subject: [PATCH 06/11] Update app/services/enrollment_service.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/services/enrollment_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/enrollment_service.py b/app/services/enrollment_service.py index 0d0d3eb..f1a1cdd 100644 --- a/app/services/enrollment_service.py +++ b/app/services/enrollment_service.py @@ -80,7 +80,7 @@ async def create_progress( now = get_current_utc_datetime() # last_position을 duration_seconds 이하로 제한 - last_position = min(data.last_position, lecture.duration_seconds) if lecture.duration_seconds > 0 else data.last_position + last_position = min(data.last_position, lecture.duration_seconds) if lecture.duration_seconds > 0 else 0 # unique_watched_seconds 계산 (진행률 계산용) # last_position을 기준으로 유니크 시청 시간 계산 From e02476665ebf0114e008718c81f7a997c8a0f49e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:43:53 +0000 Subject: [PATCH 07/11] Initial plan From 26136d3a19bff571e4424f7a15871940c0091404 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:47:21 +0000 Subject: [PATCH 08/11] Update documentation for unique_watched_seconds to reflect max position tracking Co-authored-by: b1351b1 <223806567+b1351b1@users.noreply.github.com> --- app/models/progress.py | 2 +- app/services/enrollment_service.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/progress.py b/app/models/progress.py index 0c7c6a5..6ae12f7 100644 --- a/app/models/progress.py +++ b/app/models/progress.py @@ -40,7 +40,7 @@ class Progress(Base): # 진행 정보 watched_seconds = Column(Integer, nullable=False, default=0, comment="총 시청 시간 (초) - 반복 재생 포함 누적") - unique_watched_seconds = Column(Integer, nullable=False, default=0, comment="유니크 시청 시간 (초) - 진행률 계산용") + unique_watched_seconds = Column(Integer, nullable=False, default=0, comment="최대 도달 위치 (초) - 진행률 계산용, 되감기 시에도 감소하지 않음") last_position = Column(Integer, nullable=False, default=0, comment="마지막 시청 위치 (초)") is_completed = Column(Boolean, nullable=False, default=False, comment="완료 여부") diff --git a/app/services/enrollment_service.py b/app/services/enrollment_service.py index f1a1cdd..8fda504 100644 --- a/app/services/enrollment_service.py +++ b/app/services/enrollment_service.py @@ -82,8 +82,8 @@ async def create_progress( # last_position을 duration_seconds 이하로 제한 last_position = min(data.last_position, lecture.duration_seconds) if lecture.duration_seconds > 0 else 0 - # unique_watched_seconds 계산 (진행률 계산용) - # last_position을 기준으로 유니크 시청 시간 계산 + # 진행률 계산용 시청 시간 (최대 도달 위치 추적) + # 첫 생성 시 last_position을 초기값으로 설정 unique_watched_seconds = last_position # 시청률이 95% 이상이면 자동으로 완료 처리 @@ -146,8 +146,8 @@ async def update_progress( # last_position을 duration_seconds 이하로 제한 last_position = min(data.last_position, progress.lecture.duration_seconds) if progress.lecture.duration_seconds > 0 else 0 - # unique_watched_seconds 계산 (진행률 계산용) - # last_position 값을 unique_watched_seconds로 사용 + # 진행률 계산용 시청 시간 (최대 도달 위치 추적) + # 사용자가 되감기를 해도 이전 최대 위치를 유지하여 진행률이 감소하지 않도록 함 unique_watched_seconds = max(progress.unique_watched_seconds or 0, last_position) # 업데이트 From 97e91456cbb94c0be83aa7a8244e6c6ff04c9a38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:52:26 +0000 Subject: [PATCH 09/11] Initial plan From bd4f5830b570177f655dadbec98c0f370f74880d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:57:33 +0000 Subject: [PATCH 10/11] Update remaining comment to use consistent terminology for unique_watched_seconds Co-authored-by: b1351b1 <223806567+b1351b1@users.noreply.github.com> --- app/services/enrollment_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/enrollment_service.py b/app/services/enrollment_service.py index 8fda504..f2c68f8 100644 --- a/app/services/enrollment_service.py +++ b/app/services/enrollment_service.py @@ -234,7 +234,7 @@ async def get_course_progress( # watched_seconds는 누적 시청 시간 (분석용) watched_seconds = progress.watched_seconds if progress else 0 - # unique_watched_seconds는 유니크 시청 시간 (진행률 계산용) + # unique_watched_seconds는 최대 도달 위치 (진행률 계산용) unique_watched_seconds = progress.unique_watched_seconds if progress else 0 last_position = progress.last_position if progress else 0 is_completed = progress.is_completed if progress else False From b1d19e8b54e01b52b3deac889fa72b554df87740 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:59:37 +0000 Subject: [PATCH 11/11] Initial plan