Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ebfcd14
프로세스 직접 실행 인자 전달을 추가한다
lsh930309 Jun 22, 2026
4298d68
리뷰 지적에 맞춰 실행 인자 경계를 보강한다
lsh930309 Jun 22, 2026
23243d8
데이터베이스 보호 중심으로 Stage 1을 경량화한다
lsh930309 Aug 21, 2026
2e8b9e8
macOS 설정 창을 단일 AppKit 경로로 전환한다
lsh930309 Aug 20, 2026
e32e922
macOS 설정 탭 동작과 배치를 복원한다
lsh930309 Aug 20, 2026
bfafc83
Windows GUI를 PySide6 기반 후보로 재구성한다
lsh930309 Aug 21, 2026
1075576
Windows 빌드 환경을 Python 3.14로 자동 정리한다
lsh930309 Aug 21, 2026
e699c01
Windows 실기기에서 확인된 DB 파일 경계를 수정한다
lsh930309 Aug 21, 2026
6c64df6
DB 복원 테스트의 SQLite 연결을 명시적으로 닫는다
lsh930309 Aug 21, 2026
b2b468d
무인 업데이트의 설치 확인창 대기를 제거한다
lsh930309 Aug 21, 2026
7b40897
Windows 메인 창을 가로형 게임 카드로 통합한다
lsh930309 Aug 22, 2026
d2c3f4c
사이드바 컨트롤의 시각 언어를 통일한다
lsh930309 Aug 22, 2026
775e611
Windows Widgets GUI를 무스크롤 콘텐츠 피팅 구조로 교정
lsh930309 Aug 22, 2026
742d22e
Windows 메인 게임 목록을 압축형 표로 복원한다
lsh930309 Aug 22, 2026
0b5b29a
Windows 메인 창의 그리드 균형과 상태 표시를 개선한다
lsh930309 Aug 22, 2026
907e97c
Windows 창 배치와 세부 정렬을 교정한다
lsh930309 Aug 22, 2026
c64e99a
Windows 메뉴 정렬과 창 스냅 경로를 교정한다
lsh930309 Aug 22, 2026
f8ab419
메뉴바 코너의 수직 정렬과 아이콘 대비를 보정한다
lsh930309 Aug 22, 2026
d8d94b0
플랫폼별 메뉴 액션 중심선에 코너 컨트롤을 맞춘다
lsh930309 Aug 22, 2026
0fde14c
메뉴바 코너의 텍스트 폭을 다시 계산한다
lsh930309 Aug 22, 2026
49cfdf6
Windows Widgets의 상호작용 피드백을 교정한다
lsh930309 Aug 24, 2026
1206a9e
사이드바 음소거와 진행률 배경 표시를 교정한다
lsh930309 Aug 25, 2026
2b5cbed
GUI 백그라운드 작업과 생명주기 기록을 단일 경계로 전환
lsh930309 Aug 25, 2026
b3c9c24
사이드바 네이티브 자원과 종료 대기 범위를 정리
lsh930309 Aug 25, 2026
9d29f19
대시보드 요청 제한과 런타임 오류 경로를 정리
lsh930309 Aug 25, 2026
c2623a8
GUI 안정성 회귀 검증을 직접 계약 중심으로 보강
lsh930309 Aug 25, 2026
38846bb
Windows Tailscale 출력의 CP949 오해석을 수정
lsh930309 Aug 25, 2026
543b7ff
사이드바 음소거 표현 테스트의 실행 상태 유입을 차단
lsh930309 Aug 25, 2026
b3aae5f
Beholder 사건 중복 생성과 lifecycle 재시도 교정
lsh930309 Aug 25, 2026
8904898
Beholder 모달 재진입 방지와 사용자 안내 개선
lsh930309 Aug 25, 2026
5f58345
CodeRabbit 실제 결함과 런타임 경계를 선별 교정한다
lsh930309 Aug 26, 2026
493f332
Windows 호스트 GUI 현대화와 안정화 변경을 병합한다 (#40)
lsh930309 Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ HomeworkHelper는 Windows 호스트 앱과 macOS 메뉴바 원격 클라이언

## 현재 지원 범위

- **Windows host app**: PyQt 기반 메인 GUI, 프로세스/웹 바로가기 관리, 세션 기록, 알림, 사이드바, 스크린샷/OBS 보조 기능.
- **Windows host app**: PySide6 기반 메인 GUI, 프로세스/웹 바로가기 관리, 세션 기록, 알림, 사이드바, 스크린샷/OBS 보조 기능.
- **Remote Agent**: 호스트 앱의 FastAPI 서버를 통해 상태 조회, 프로세스 실행/종료, 대시보드 요약, pairing/token 기반 보호 endpoint를 제공합니다.
- **macOS remote client**: 메뉴바 popover 중심의 네이티브 Swift 클라이언트입니다. pairing, host 상태 확인, Moonlight 실행 상태 반영, 원격 quick action을 담당합니다.
- **Dashboard frontend**: `src/api/dashboard/frontend`의 Vite/React 앱을 빌드 시 `build/dashboard-static`으로 생성해 PyInstaller 패키지에 포함합니다.
Expand All @@ -22,6 +22,9 @@ pip install -r requirements.txt
python homework_helper.pyw
```

기본 화면은 modernized Widgets이며 독립 Qt Quick 후보 실행 및 검증 방법은
[`docs/development/windows-gui-modernization.md`](docs/development/windows-gui-modernization.md)에 정리되어 있습니다.

서버만 확인할 때는 GUI 단일 인스턴스 경로를 우회합니다.

```bash
Expand Down
143 changes: 142 additions & 1 deletion build.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import platform
import argparse
import hashlib
import importlib.metadata
from pathlib import Path
from datetime import datetime, timedelta
try:
Expand Down Expand Up @@ -65,6 +66,16 @@
VERSION_BUMP_CHOICES = ("none", "build", "patch", "minor", "major")
DEFAULT_VERSION_BUMP = "build"
VERSION_PATTERN = re.compile(r'v(\d+)\.(\d+)\.(\d+)_b(\d+)_g([0-9a-fA-F]+|unknown)(?:_dirty)?')
WINDOWS_PYTHON_VERSION = (3, 14)
WINDOWS_REQUIRED_DISTRIBUTIONS = (
"PySide6",
"PyInstaller",
"pywin32",
"winshell",
"pycaw",
"Windows-Toasts",
"winrt-Windows.Gaming.Input",
)

# 코드 서명
CERT_DIR = PROJECT_ROOT / "certs"
Expand All @@ -80,6 +91,122 @@ class BuildConfigError(RuntimeError):
"""Raised when local build configuration is missing or invalid."""


def configure_console_output(stdout=None, stderr=None) -> None:
"""Replace glyphs unsupported by the active Windows console encoding."""
streams = (
sys.stdout if stdout is None else stdout,
sys.stderr if stderr is None else stderr,
)
for stream in streams:
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure is None:
continue
try:
reconfigure(errors="replace")
except (OSError, ValueError):
pass


def bootstrap_windows_build_runtime(
argv: list[str],
*,
system_name: str | None = None,
project_root: Path = PROJECT_ROOT,
python_executable: str | None = None,
python_version: tuple[int, int] | None = None,
runner=subprocess.run,
launcher_finder=shutil.which,
) -> int | None:
"""Update the repository .venv and delegate without shell activation."""
if (system_name or platform.system()) != "Windows":
return None

current_python = Path(python_executable or sys.executable)
current_version = python_version or tuple(sys.version_info[:2])
managed_python = project_root / ".venv" / "Scripts" / "python.exe"

if not managed_python.exists():
if current_version == WINDOWS_PYTHON_VERSION:
launcher = [str(current_python)]
else:
py_launcher = launcher_finder("py")
if not py_launcher:
raise BuildConfigError(
"Python 3.14가 필요합니다. Python Launcher를 포함해 Python 3.14를 "
"설치한 뒤 build.py를 다시 실행하세요."
)
launcher = [str(py_launcher), "-3.14"]
print(f"[준비] Windows 프로젝트 가상환경 생성: {managed_python.parent.parent}")
created = runner([*launcher, "-m", "venv", str(managed_python.parent.parent)], cwd=project_root)
if created.returncode != 0 or not managed_python.exists():
raise BuildConfigError("Windows 프로젝트 Python 3.14 가상환경 생성에 실패했습니다.")

if current_python.resolve() != managed_python.resolve():
delegated = runner(
[str(managed_python), str(project_root / "build.py"), *argv],
cwd=project_root,
)
return int(delegated.returncode)

print("[준비] requirements.txt 의존성 설치 및 업데이트")
installed = runner(
[
str(managed_python),
"-m",
"pip",
"install",
"--disable-pip-version-check",
"--upgrade",
"-r",
str(project_root / "requirements.txt"),
],
cwd=project_root,
)
if installed.returncode != 0:
raise BuildConfigError("Windows 빌드 의존성 설치에 실패했습니다.")
return None


def installed_distribution_versions(names: tuple[str, ...]) -> dict[str, str]:
versions = {}
for name in names:
try:
versions[name] = importlib.metadata.version(name)
except importlib.metadata.PackageNotFoundError:
versions[name] = "missing"
return versions


def validate_windows_build_runtime(
*,
python_version: tuple[int, int] | None = None,
distribution_versions=None,
) -> dict[str, str]:
"""Require Python 3.14, build dependencies, and one Qt binding."""
current_version = python_version or tuple(sys.version_info[:2])
if current_version != WINDOWS_PYTHON_VERSION:
raise BuildConfigError(
"Windows 빌드는 Python 3.14가 필요합니다. "
f"현재 버전: {current_version[0]}.{current_version[1]}"
)

version_reader = distribution_versions or installed_distribution_versions
versions = version_reader(WINDOWS_REQUIRED_DISTRIBUTIONS)
missing = [name for name, version in versions.items() if version == "missing"]
if missing:
raise BuildConfigError("Windows 빌드 필수 패키지가 없습니다: " + ", ".join(missing))

bindings = version_reader(("PySide6", "PyQt6"))
installed_bindings = [name for name, version in bindings.items() if version != "missing"]
if installed_bindings != ["PySide6"]:
found = ", ".join(installed_bindings) if installed_bindings else "없음"
raise BuildConfigError(
"Windows 빌드 환경에는 PySide6만 설치되어야 합니다. "
f"현재 감지된 Qt 바인딩: {found}"
)
return versions


def select_build_target(system_name: str | None = None) -> str:
"""Return the release target for the current OS."""
system_name = system_name or platform.system()
Expand Down Expand Up @@ -1198,6 +1325,8 @@ def build_with_pyinstaller(gui):
gui.log(f"빌드 명령: {' '.join(cmd)}\n")

try:
pyinstaller_env = dict(os.environ)
pyinstaller_env["QT_API"] = "PySide6"
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
Expand All @@ -1206,6 +1335,7 @@ def build_with_pyinstaller(gui):
encoding='utf-8',
errors='replace',
cwd=PROJECT_ROOT,
env=pyinstaller_env,
bufsize=1
)

Expand Down Expand Up @@ -2090,11 +2220,22 @@ def create_candidate_version_config(

def main(argv: list[str] | None = None):
"""메인 함수"""
args = parse_args(argv)
configure_console_output()
effective_argv = list(sys.argv[1:] if argv is None else argv)
args = parse_args(effective_argv)
try:
delegated_exit = bootstrap_windows_build_runtime(effective_argv)
except BuildConfigError as exc:
print(f"[오류] {exc}")
return 1
if delegated_exit is not None:
return delegated_exit
# 커스텀 폰트 로딩
try:
target = args.target or select_build_target()
validate_target_inputs(target)
if target == "windows-host":
validate_windows_build_runtime()
version_config = load_version_config(args.version_file)
candidate_config = create_candidate_version_config(
target,
Expand Down
Loading