From 44e2fb2e44f320c504b487baae0cedfc2ad7aa61 Mon Sep 17 00:00:00 2001 From: hyungi Date: Thu, 18 Sep 2025 07:00:28 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=82=AC=EC=A7=84=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사진 2장까지 업로드 지원 - 카메라 촬영 + 갤러리 선택 분리 - 이미지 압축 및 최적화 (ImageUtils) - iPhone .mpo 파일 JPEG 변환 지원 - 카테고리 변경: 치수불량 → 설계미스, 검사미스 추가 - KST 시간대 설정 - URL 해시 처리로 목록관리 페이지 이동 개선 - 로그인 OAuth2 form-data 형식 수정 - 업로드 속도 개선 및 프로그레스바 추가 --- backend/__pycache__/main.cpython-311.pyc | Bin 3052 -> 3129 bytes .../__pycache__/models.cpython-311.pyc | Bin 4542 -> 4875 bytes .../__pycache__/schemas.cpython-311.pyc | Bin 8861 -> 9079 bytes backend/database/models.py | 37 +- backend/database/schemas.py | 6 +- backend/main.py | 2 + backend/migrations/001_init.sql | 31 +- backend/migrations/002_add_second_photo.sql | 5 + backend/migrations/003_update_categories.sql | 8 + .../migrations/004_fix_category_values.sql | 9 + backend/migrations/005_recreate_enum_type.sql | 20 + .../routers/__pycache__/auth.cpython-311.pyc | Bin 8679 -> 8683 bytes .../__pycache__/daily_work.cpython-311.pyc | Bin 8794 -> 8865 bytes .../__pycache__/issues.cpython-311.pyc | Bin 8225 -> 8212 bytes .../__pycache__/reports.cpython-311.pyc | Bin 6792 -> 6792 bytes backend/routers/auth.py | 6 +- backend/routers/daily_work.py | 6 +- backend/routers/issues.py | 45 +- backend/routers/reports.py | 4 +- .../__pycache__/auth_service.cpython-311.pyc | Bin 4668 -> 4668 bytes .../__pycache__/file_service.cpython-311.pyc | Bin 3514 -> 3866 bytes backend/services/auth_service.py | 2 +- backend/services/file_service.py | 23 +- docker-compose.yml | 3 + frontend/admin.html | 3 +- frontend/daily-work.html | 11 +- frontend/index.html | 549 +++++++++++++++--- frontend/issue-view.html | 77 ++- frontend/static/js/api.js | 37 +- frontend/static/js/date-utils.js | 139 +++++ frontend/static/js/image-utils.js | 134 +++++ nginx/nginx.conf | 8 + 32 files changed, 988 insertions(+), 177 deletions(-) create mode 100644 backend/migrations/002_add_second_photo.sql create mode 100644 backend/migrations/003_update_categories.sql create mode 100644 backend/migrations/004_fix_category_values.sql create mode 100644 backend/migrations/005_recreate_enum_type.sql create mode 100644 frontend/static/js/date-utils.js create mode 100644 frontend/static/js/image-utils.js diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index 739167f05e83f7b8f5654b86de2079157ee35b06..c495f2e495d633f9b1bff956cb3aede71d7094df 100644 GIT binary patch delta 356 zcmaDOzEgsCIWI340}#B5I+f8okynz@WTN`?$v;>G_)>YYL_jhiut02bBBSucy#}n} zsp1PHK%B{}j4I6H3nW2oR)$o*EUC#47$uQJ_%TETFhm65BGOxwf_dIR zo*bOl4&=!Lc?uvoZ-!LGWsD3AtAQ8-Qp8i4QY2Cp(^yg@TUeF>1y{qQ7^0L?l~bg$ znLq+XqD%~_>{%)>$yD*pe9Rt9jOvraS(h{FP5#e1k`{Uj}B7#v+&Cc5Reyy$OCPYnyklU&MFPGUuJR^kR=OdEn_lImIJcn(QO9t zQx%pmGBB(LVhBhPPi0DxNL5H<2X54jB^o}7f=t#zT)V~ Shd9@s?tzEK`cN`9#aQ(X)AUT57~iZu`J#ydg9+J0A51i{mV~B_5A{#{zBx+E5$Bj6 zv)`YYZ@&3vzUsN$*Ys&PY!YZ==f0i#$y{h^Ro+_YKSc|+V;3S$L?M*W;7rts1_-mE zN2p$HSW*bN3BA^~e>pK~%oC>>Y^^0QxcSJ8IPm~^nb6QYp(abP=)(7Hc8e5QAZmOh zN=i`}L`|EbGp$l&gDAq{ODaqXz1EyIsf&UxwnMi|>Y70p-=S-#!BZri5Yx(qIn_64 z)?;42zWnW$xx4Esp;&672U60j?2mD=mafZ;d?- zl$&jPx*!H+u-p#1^~L$SA6L89uU^0N#b;f2FJHfNYrgA`TZ`)-ET&c71QxzpEM4%` z6Q@p#=|I;3-B3ckhL@NTrkcfz@fo@$Y+QBb3vKuvf5M78KL@(kgMBOwglp*5ctlC$bCWXuQ z;xyL?BaT9e6QGKt(tx8;)d^C~(dCKg;u|$J*GQS0pDbn==Oymj`C_hA$QLIwl$~ce z4+ZWl4DzL7rjU2t@pJ>P3vn+7kw)x8>_^na*^e>==FY`ERW^Tp6|$C0FKh9&=I+XC zYc2hi35m)?WulfZbjvM76Ur`e+d3qE(>{3y?nMAf6MC)5I}oP^4OPuLO#*I5Tznl& zC@td0;BagZJba^Ao|(y>n_)K;eo%DkuZELPZc2FiNnM^$r}$3aqhZkEPrZM4-L9Sn zIY;1exj%@}h7ZmC%hBPL=?JOtosTZWB#gmiG3VX7+RhCYPPD1Zvtha_}HbmiR@ESKqgw9&zQvY>f%gY0C&bTY*&JPtjjY5*R%0U~V;!xX3;M`& zIrjN&E2+#?jk*kC#^ThZO4X{e9ta7Z=}Lw!2sC?p2_kTGdk(bwdj%of~8e1~FR2 zyOI2WrtIAyJ8|X3#rXBMd#HKvSluYPqeJ^_<;VuviA%@sq4dFHRZ6@QJ)&Xe{sR`f BpDzFa delta 1588 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 delta 1970 zcmZWpUrbwN6z>POh2H)hw7_klr6907ShAusps=uiNW&=G%v`{^uC^E30WG^NCK@x0 znMM;HCg)*h2@iXjF566W@lCfFpY&;oFQ!r7OnfqskQhz;^Zm}PtI*rtU(P+}_nmXT z^PTVfKDl|)`?1HP+T=4D_%5fpKl6T}&|i%)`c;Yb`Lp?Dqfp8h3#qKJU}VY_U%rqj zF6RrcRTW9yLC@QxWGFr=*u9~sboBO<-w*BjVfJ@Mk~jdm@@lcTWcp#=2WSE4|3MesX}%(Kj(7gJd{jf9+(JyLjXhcd$z1j3&jThjPP-)B;%2r~7 z(Afba>^u$;FhH}e%OcM4zjrOWYI}(@aSTMjA43t`tNHm+Vtj%73SPZ7GcbjSUySr`1?=8v+mnl6ee&y4OHI9BqT2ODJfO#3rodx z*_?qk2?ziZ*sX6GA^P@x8X3Fkcah1f)j)W+Y7d=dd-$~-EOei?#kDC<~`X4xo3C9&ZRYnr8-$rxw5I1 zbU9}xX|i>{1G_M1=&Lr3)?^;HTm9l$WS>TBn&&0F1Ys1w*8qCmySG%&!OjIb3&}bp zNL&x@TJGXm=(t+k&%QJG1|W~pC$tjMXfY5V3S1LsxIN;*pPypK3aGCDIM_u<947^7 zmK$siB4D0Qw=Ib|xvxuV8cM(=8ffnlX_TEVwr}`fhJih@v(!dMD9WGgwzeh93bGX? z0}+ssQG&GCab9HD>5m=nh+#@;)8gqT75zyISQXW{qDykw6|DbXMJW<$3)RALjcO&c zF1~>^fE+D`Hic2WW`i^n9-~{{fLZ_nu*zfS%W$tKFzJu*+^`vjwNparRS@~y~8KYC(Nh9W#UqB?wrQ1IW7n7 zpUq>-#q8>mam0L!($Pmljbi_ntsbr)|0u8x{Z7s3esHS$O|d3K_m-_5uIt;-?$k^U l2blE$rqu4#On#S$ZrSSL{lqr3J2g|MLb2}l2N!z6;$I~EkrDs^ diff --git a/backend/database/models.py b/backend/database/models.py index b4f24f6..77943b2 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -1,24 +1,32 @@ from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, ForeignKey, Enum from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship -from datetime import datetime +from datetime import datetime, timezone, timedelta import enum +# 한국 시간대 설정 +KST = timezone(timedelta(hours=9)) + +def get_kst_now(): + """현재 한국 시간 반환""" + return datetime.now(KST) + Base = declarative_base() class UserRole(str, enum.Enum): - ADMIN = "admin" - USER = "user" + admin = "admin" + user = "user" class IssueStatus(str, enum.Enum): - NEW = "new" - PROGRESS = "progress" - COMPLETE = "complete" + new = "new" + progress = "progress" + complete = "complete" class IssueCategory(str, enum.Enum): - MATERIAL_MISSING = "material_missing" - DIMENSION_DEFECT = "dimension_defect" - INCOMING_DEFECT = "incoming_defect" + material_missing = "material_missing" + design_error = "design_error" # 설계미스 (기존 dimension_defect 대체) + incoming_defect = "incoming_defect" + inspection_miss = "inspection_miss" # 검사미스 (신규 추가) class User(Base): __tablename__ = "users" @@ -27,9 +35,9 @@ class User(Base): username = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=False) full_name = Column(String) - role = Column(Enum(UserRole), default=UserRole.USER) + role = Column(Enum(UserRole), default=UserRole.user) is_active = Column(Boolean, default=True) - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=get_kst_now) # Relationships issues = relationship("Issue", back_populates="reporter") @@ -40,11 +48,12 @@ class Issue(Base): id = Column(Integer, primary_key=True, index=True) photo_path = Column(String) + photo_path2 = Column(String) # 두 번째 사진 경로 category = Column(Enum(IssueCategory), nullable=False) description = Column(Text, nullable=False) - status = Column(Enum(IssueStatus), default=IssueStatus.NEW) + status = Column(Enum(IssueStatus), default=IssueStatus.new) reporter_id = Column(Integer, ForeignKey("users.id")) - report_date = Column(DateTime, default=datetime.utcnow) + report_date = Column(DateTime, default=get_kst_now) work_hours = Column(Float, default=0) detail_notes = Column(Text) @@ -63,7 +72,7 @@ class DailyWork(Base): overtime_total = Column(Float, default=0) total_hours = Column(Float, nullable=False) created_by_id = Column(Integer, ForeignKey("users.id")) - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=get_kst_now) # Relationships created_by = relationship("User", back_populates="daily_works") diff --git a/backend/database/schemas.py b/backend/database/schemas.py index 34e5893..d5d3014 100644 --- a/backend/database/schemas.py +++ b/backend/database/schemas.py @@ -14,8 +14,9 @@ class IssueStatus(str, Enum): class IssueCategory(str, Enum): material_missing = "material_missing" - dimension_defect = "dimension_defect" + design_error = "design_error" # 설계미스 (기존 dimension_defect 대체) incoming_defect = "incoming_defect" + inspection_miss = "inspection_miss" # 검사미스 (신규 추가) # User schemas class UserBase(BaseModel): @@ -64,6 +65,7 @@ class IssueBase(BaseModel): class IssueCreate(IssueBase): photo: Optional[str] = None # Base64 encoded image + photo2: Optional[str] = None # Second Base64 encoded image class IssueUpdate(BaseModel): category: Optional[IssueCategory] = None @@ -72,10 +74,12 @@ class IssueUpdate(BaseModel): detail_notes: Optional[str] = None status: Optional[IssueStatus] = None photo: Optional[str] = None # Base64 encoded image for update + photo2: Optional[str] = None # Second Base64 encoded image for update class Issue(IssueBase): id: int photo_path: Optional[str] = None + photo_path2: Optional[str] = None # 두 번째 사진 경로 status: IssueStatus reporter_id: int reporter: User diff --git a/backend/main.py b/backend/main.py index e333629..ba57295 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,6 +9,8 @@ from routers import auth, issues, daily_work, reports from services.auth_service import create_admin_user # 데이터베이스 테이블 생성 +# 메타데이터 캐시 클리어 +Base.metadata.clear() Base.metadata.create_all(bind=engine) # FastAPI 앱 생성 diff --git a/backend/migrations/001_init.sql b/backend/migrations/001_init.sql index 9352b69..32e1fba 100644 --- a/backend/migrations/001_init.sql +++ b/backend/migrations/001_init.sql @@ -1,10 +1,22 @@ +-- 초기 데이터베이스 설정 + +-- Enum 타입 생성 (4개 카테고리) +CREATE TYPE userRole AS ENUM ('admin', 'user'); +CREATE TYPE issueStatus AS ENUM ('new', 'progress', 'complete'); +CREATE TYPE issueCategory AS ENUM ( + 'material_missing', -- 자재누락 + 'design_error', -- 설계미스 + 'incoming_defect', -- 입고자재 불량 + 'inspection_miss' -- 검사미스 +); + -- 사용자 테이블 CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, hashed_password VARCHAR(255) NOT NULL, full_name VARCHAR(100), - role VARCHAR(20) DEFAULT 'user', + role userRole DEFAULT 'user', is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -12,20 +24,21 @@ CREATE TABLE IF NOT EXISTS users ( -- 이슈 테이블 CREATE TABLE IF NOT EXISTS issues ( id SERIAL PRIMARY KEY, - photo_path VARCHAR(255), - category VARCHAR(50) NOT NULL, + photo_path VARCHAR(500), + photo_path2 VARCHAR(500), -- 두 번째 사진 + category issueCategory NOT NULL, description TEXT NOT NULL, - status VARCHAR(20) DEFAULT 'new', + status issueStatus DEFAULT 'new', reporter_id INTEGER REFERENCES users(id), report_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, work_hours FLOAT DEFAULT 0, detail_notes TEXT ); --- 일일 공수 테이블 +-- 일일 작업 테이블 CREATE TABLE IF NOT EXISTS daily_works ( id SERIAL PRIMARY KEY, - date DATE NOT NULL UNIQUE, + date DATE NOT NULL, worker_count INTEGER NOT NULL, regular_hours FLOAT NOT NULL, overtime_workers INTEGER DEFAULT 0, @@ -33,11 +46,13 @@ CREATE TABLE IF NOT EXISTS daily_works ( overtime_total FLOAT DEFAULT 0, total_hours FLOAT NOT NULL, created_by_id INTEGER REFERENCES users(id), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(date) ); -- 인덱스 생성 CREATE INDEX idx_issues_reporter_id ON issues(reporter_id); CREATE INDEX idx_issues_status ON issues(status); -CREATE INDEX idx_issues_report_date ON issues(report_date); +CREATE INDEX idx_issues_category ON issues(category); CREATE INDEX idx_daily_works_date ON daily_works(date); +CREATE INDEX idx_daily_works_created_by_id ON daily_works(created_by_id); \ No newline at end of file diff --git a/backend/migrations/002_add_second_photo.sql b/backend/migrations/002_add_second_photo.sql new file mode 100644 index 0000000..d459851 --- /dev/null +++ b/backend/migrations/002_add_second_photo.sql @@ -0,0 +1,5 @@ +-- 두 번째 사진 경로 추가 +ALTER TABLE issues ADD COLUMN IF NOT EXISTS photo_path2 VARCHAR(500); + +-- 인덱스 추가 (선택사항) +CREATE INDEX IF NOT EXISTS idx_issues_photo_path2 ON issues(photo_path2); diff --git a/backend/migrations/003_update_categories.sql b/backend/migrations/003_update_categories.sql new file mode 100644 index 0000000..80f5948 --- /dev/null +++ b/backend/migrations/003_update_categories.sql @@ -0,0 +1,8 @@ +-- 카테고리 업데이트 마이그레이션 +-- dimension_defect를 design_error로 변경 +UPDATE issues +SET category = 'design_error' +WHERE category = 'dimension_defect'; + +-- PostgreSQL enum 타입 업데이트 (필요한 경우) +-- 기존 enum 타입 확인 후 필요시 재생성 diff --git a/backend/migrations/004_fix_category_values.sql b/backend/migrations/004_fix_category_values.sql new file mode 100644 index 0000000..7e44509 --- /dev/null +++ b/backend/migrations/004_fix_category_values.sql @@ -0,0 +1,9 @@ +-- 카테고리 값 정규화 (대문자를 소문자로 변경) +-- 기존 DIMENSION_DEFECT를 design_error로 변경 +UPDATE issues SET category = 'design_error' WHERE category IN ('DIMENSION_DEFECT', 'dimension_defect'); +UPDATE issues SET category = 'material_missing' WHERE category = 'MATERIAL_MISSING'; +UPDATE issues SET category = 'incoming_defect' WHERE category = 'INCOMING_DEFECT'; +UPDATE issues SET category = 'inspection_miss' WHERE category = 'INSPECTION_MISS'; + +-- 카테고리 값 확인 +SELECT category, COUNT(*) FROM issues GROUP BY category; diff --git a/backend/migrations/005_recreate_enum_type.sql b/backend/migrations/005_recreate_enum_type.sql new file mode 100644 index 0000000..e90e22b --- /dev/null +++ b/backend/migrations/005_recreate_enum_type.sql @@ -0,0 +1,20 @@ +-- PostgreSQL enum 타입 재생성 +-- 카테고리 컬럼을 임시로 텍스트로 변경 +ALTER TABLE issues ALTER COLUMN category TYPE VARCHAR(50); + +-- 기존 enum 타입 삭제 +DROP TYPE IF EXISTS issuecategory CASCADE; + +-- 새로운 enum 타입 생성 (4개 카테고리) +CREATE TYPE issuecategory AS ENUM ( + 'material_missing', -- 자재누락 + 'design_error', -- 설계미스 (기존 dimension_defect 대체) + 'incoming_defect', -- 입고자재 불량 + 'inspection_miss' -- 검사미스 (신규) +); + +-- 카테고리 컬럼을 새 enum 타입으로 변경 +ALTER TABLE issues ALTER COLUMN category TYPE issuecategory USING category::issuecategory; + +-- 확인 +SELECT enumlabel FROM pg_enum WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'issuecategory'); diff --git a/backend/routers/__pycache__/auth.cpython-311.pyc b/backend/routers/__pycache__/auth.cpython-311.pyc index e209c51bb0a62edc0e719cb8d24465c22814c579..7cb5703ffea56a318d285f3007021c1e71f55912 100644 GIT binary patch delta 307 zcmaFv{Mwm!IWI340}yb>oXSv|$ScXXV5534Bik~d&}xQ>ALJE9Q`uAaQ#sREQp8$V zA;KW_AP}XHD!D*$vZA2Kyo_0uRTC(q1(tcj9L=ab*?}dTQD^cD7F9;w&HGqF zSy>ZPax?QLOK`1coXQZH$ScV>W21U7Af*zRc*ys65$$QF5|1lMkcH}ToW1jrLHKNY{k7Bm=6;?ien$PpucR6IZt?hlGz6sWu$}0VlJ3FV8diZ7z9@ ztzY`C=eG&iz}p(ITV~0_y1Go0VBXudp(&Q3X{Ob6n)yWA2KBx6l4O;@M;Uz>;G+Y* z2$wu{;WTb^+RgfCj`fL$WQb(MAEc;N+H^WC?uC3RE}8ZM7X&IAa#8CC7PFOjn`P!S z5w%##;CP^&VZ`|eI0u1q2sy=sl3P4I+kWvX$BH-H&Q^S>6mutXiXWAyORe%7)I7#8 zB20B@N(~Ka2P|+r7OE(P2FkbB8!SA75@l{+S4pU^v0#O+R#owtTGB#g@r^fkwpCZD zCL!NYuRCtQILE^pYITSE0oOq0Cc+@XkeD&d;R1?91Uw|6!9H^Qz-g>;EE^w27!jMs z0-41doPr*gJNbxir*1 zwB07-ufu*HdxaG-&Cb@xyv!%Wovu8&vGYyWCV^^9=RPNgtI;z~D&n7>(JTMkgHYw` zF5_wN5NbB{oJOd_@nmrDqFma z7m4l2R z8gOn;JgsPX5dq`k?*jM+z+MgEfQhAYFR2Ts{78?SofTf+LY>FZJPr_M;QHFDw$Ey9 zfhSO(YENt4I+V+AU8fZ8!&34L8sT3c+RU>!jq~*;l+~}<&EO{Q5{b%CV-k(8;i}>^ z0)7dn^U-vp8HA}hyk2j??Yh1JT#Vnqbqiq`p^oqx0!AO*m%ojdAfl$rF7g0$1YZAp rMUwth2q7ZfZ;A(%b8o+}D?Q=)RT|zs{QNAvxcj7Mr`A8Fs2I$D(B6>7 delta 1815 zcma)7&u<$=6yDkOI;(o^wH-Uj+Ogxrp{+~f0ELzxp`{@}Lq&w>p~aC1+sq_g*p18V znzS6Kgt#IU&5ctz(5fmb2w(db6b|8rMhFnx;fPca;=OeoY^Oz8tv|ngGw=J}y!Y1r z{3W@NyqQcMP~iF~aqrTPTS;5JuWcQkf0`&%p}Lpw&4wA-aWCno8Y$muSbn;Zj(kI2 z#?Lmgk!^Uxey)+*x>M5gEPs7Oe6Q!?)MVLrQsObSbRu_yD9Ua4bS_n4Dl_|iYbs5_ z{?YCpYB5WkB(^>VgfvesllS5%E`Ia^VRurRI2E&ESut8y)-@!jlNQ)EpzFzS-pO27v)~ zW#{5E;%#$;%!y6&8#3EIk}Q&C8`f*_C}c+rxeg&5M9=|l#rX4R|EGXw31R@q%xA!m z7H1A!p^ocaT|sF+FRrItkU3!eswPfh{0j)S_%ri5IWEp*50V+t%&uLV1$Ry3J@vwU z1CQhA>!}pRr2ju{Xpy@bLdiC+N)5U+Kq+-MWPTu*hXNKlVaRz6HI5*2s&n8M(b0eE z0g{1~q`u}dk4pW0$7?bEs`zSn)Q-xNX%D1Q5O+p#9GtPVN~Q!a ztXGdiV2$u8_`0R?GssUi0~Nk^y3NmuzYC2Rc20a&I91*Wh=%wraPHZARQy()U5`3F zG}tx#k><|=NG)jjQuEjbjQGO_n?Hx!F*UvnFficAk`ttuQ~9d+bo`%qBv}=rw0K@- zpaafDCt&ri!4nu@bsy_~6Y3LmUW6n{M7712K8oWOZZ~Kz?T6p(*2G~g>eavOynBfN diff --git a/backend/routers/__pycache__/issues.cpython-311.pyc b/backend/routers/__pycache__/issues.cpython-311.pyc index 22476d1ba1d902f97acc7daa30453f6f4516dd32..5f7acc8b578e2098bcb34b12f98fe61392235cea 100644 GIT binary patch delta 2163 zcmZuyZ)_Ar6ra7j{d0GF|F75f&bCKssn;F_Ds6y5ZP7xprA7W3l%zS>Ej@be)!97| zxu#7t1d+rhgOTV5;R|BGMAMiUKNvswMG{R*E_&IdNi{JMzqpMW6GM#e?OrJ+OmBYs z=FM;3%)EK;^=9w2UjIivpNBxJJ9%&XtJr7$Mm{0I@D1pH9OnobBpi8tKU?E$3jXo>=RQOBd0C-C)x7n9M(tokbVRMjOL-xmyAucc9yChG(2m<0$fM zm5;ErP{l^Qe)b@mi1W8#|2xp_#*rd_grpq9DS@37nxB~-&+GZo>5M+!VR)%Jou|4= zN3#k=(<#~DEdmY0$Pg^KgMBE(pPy3C8_tZP7{clNNaRU1zUv=+!Tj*viT zKu7`@l5%P^t7!#QL+eTQw~$V@f#ip-eFE?+{LK;5Pr|Xe7s>#GE38MX8CntLH;X3W z_^#zx%WeLyycFv!#X2v0{}M>BsvL@4t1m}uOVQ?Xq^=xoC`XgmMwSEdtFxw03<|3R z;7^3P0W-)hiCM=(!dAsSqJwTM@;4mprwv{zgke~1uDZq4Ph*s15a6QGKBotn5OD<3(hz*xm-%5?I2qUi}IK|wy`C5Rd*{= z!QmOD;t|>jnlyAR3s7-HkoW@c3|tv__t;w_S4ZahO%IX$Y{)arrP&9bt&V$y-Hd$8 zJK;rLOqAOj!28=^xphX`fl?EI!H;FBrc<8H%NrBc)}HmP>9at=uAO9o*a=A@a%gUd zrr9Z93-|uQ72iXyVPDa87GKF~G^*nhR~pW+P$0p1*ycdO;i+Y&r+RiqEQ&>5@$LrJ z6t6(k*)n`Rdx#}o?Md%IU8D&Cmblz!2f13O08D( z@`@j~hS!b2>oqc8(A_4@xb;+pfpe~+RCM2RLV7^A8%M5I7o9h%SYNP(i&>HIJ0#S6 z&=m~~(;YyfI}vsPJYXEV9o#kBhzcvnH=>jTFg)k;bYgToU!WSrliKN2{iMgCdvG38 z+;Gu+PNjHr!&T5Ax;9v%l0B{48$^(bx3qS#Bdg1Y<7Q+^kc-nfRaXr@tE-cmAwpDU zbe+-`Q=ffUhz>QU82q_R4pI{fS@Kb%Rt<5 zNsfhILJ@8@IN!Y}*28bb8!UOBf<$$K7Z=4w z_}#ep>F!cX@7LSzw=g+!flDu3irnY8z3fC)|LhZTVBO8MA01$Mna?Pb*{OBk8iJyt z|7?Ht;2kj{(Hj*<*z*-f9EQ2Kk@O8fC1qtuSymVO5)M?3eeRg8yv#JspMmnI(E*^Q z{Er2~qtnoRFl8q#zI!EePv;9$y8Y7l(1vrk|0P2@ba-U{;r=5>4C%R%gNKItkM={! zSueTP{g0xNZVU{13;}o6@ShA`2bz7Oz!_DyuQRGatZ4LQVBAFN&rl6&*l7HFC^qeh z-;QIYHk?2=oLM*$)+?)Y7|!W}Zg>x1VLxWYi4`IAMI@k$ER!D-iq*~9^dD*WWL_!c z)E+vqF!fX$*OTC)CIKjW!zFLM=>%~l95Z>;3&b5X#WjXSTXho0))KeHbf9_{`?0R) zXpjq=1fcBknNIkwc>N}iN{FAT`BzF1KOzn#5~()bD0$GZ7bRS00UGWsaobG?YMNNK zzN6dAg-ila_J>R-e653_(mvSQw8g>oJ4@US(}C(Z`=P#Lpc~f%SnJ`t60R+Un@t|| c0v<+0Ny5XpQ9`Z->&8&?Iy=VAE+P!$N&HU delta 2009 zcmZuxTWlLe6rJ^YA6~y5Kbo|4oo8{Nzh5CRCIffIUyXhoXb}<;OkalQ_a=^zQ-mXLlLDFH*(u?`bB2KA z+Azrkzs`2A+Puk4tnv(B-A~8_ZtaYZxSPPZwaS`to32Hf9TTJMw_pSBTJ(Uuo%M?` zm?zw>Y+7LNi0w(LPHWUq>1eS)Mc66rwS?($&CqBBX3Q3w_-y7J^Gk^~fi{ARHX&?A z*al$9h37|$x?Waw{5H!SbqQd z{n*}0Z0~EzpCSpxs^R!TvKnoyMBA$kTdUF3!r+5o)BLRM6+_}f0`LdI+zC6t{*j8V z%N#4py&TW>CDOdGD6lV-WN?_!k zJhJQ!%?;2_cGS0>6PGUf{^rzfQz$4{iM3JiPCAW3+f4TaR-H49WZi-$3Dxb%nhwGL zM8owc?CU!M6R4C3R7VSYDd_2vW@KY~odGeoDVg$3XwzG(t;&)4gvl>PVNE-`9ZX<@ zMg3WN5SGzH2zW}`OITy*@K7__PUk3BG&(stu9c}yF|k_z#Wzxy_F;TuZEuGfd$97T zAE6b%l8+Cb9vaA;$yh?sP^WZDf{M%=2Bq!D!9q%V=mE5kF*)4UgSV%a3YKs=UxH4? zB5{flp;nE)@^m3@sI_{e13)+lkNz&VG!<^(*sqb-xz44l4c~EGFB^zuW;ZC2jSgW6 z#~wTVWY!UcHo%USSWrtU^p%t8QTzs8@47^4@s&h|Aoe1Xz64n53m@?ZSt$M~B=mLs z6W6T8UsA7F@-)>hQdQS2`GPhzjo(NI)@l8}nso|01)Hu^_XVwd$*8sOI-Q4x2cH{Wm)2AI9}PIm9yvX93nay)JF=qH5HRGpfTa>2w5WZy@zYXpBv4xanJH z@8-?F#PE;R;(>1QMaTyK6lgz7ZHaf`h0s9+RK(fgLqa>z#=iSYJ~E{h$|bdr7MJo{ zy12d=7qtmM)f=gJlQs|LawKjG=og76WJ`61Lwm>*S9gWmZM)FDi{07UcP7k5YywbK z{5B8oWpBV1&?yps?9KBPU$ZUYR3;4x+k=*Z8+*~hbq?Uh`zzc&+l8JE_Dph5kDm+M z1fUuS+dRCT#L!twtR8kaxc*RuJ7~Mmon&7n_nhd$^#IO#crQm9E0K0vK);B|NNCBJ Wj0Y{`I 0: - if issue.status == IssueStatus.NEW: - update_data["status"] = IssueStatus.COMPLETE + if issue.status == IssueStatus.new: + update_data["status"] = IssueStatus.complete for field, value in update_data.items(): setattr(issue, field, value) @@ -128,7 +147,7 @@ async def delete_issue( raise HTTPException(status_code=404, detail="Issue not found") # 권한 확인 (관리자만 삭제 가능) - if current_user.role != UserRole.ADMIN: + if current_user.role != UserRole.admin: raise HTTPException(status_code=403, detail="Only admin can delete issues") # 이미지 파일 삭제 @@ -148,7 +167,7 @@ async def get_issue_stats( query = db.query(Issue) # 일반 사용자는 자신의 이슈만 - if current_user.role == UserRole.USER: + if current_user.role == UserRole.user: query = query.filter(Issue.reporter_id == current_user.id) total = query.count() diff --git a/backend/routers/reports.py b/backend/routers/reports.py index 7e5d5f5..5bf90f9 100644 --- a/backend/routers/reports.py +++ b/backend/routers/reports.py @@ -35,7 +35,7 @@ async def generate_report_summary( ) # 일반 사용자는 자신의 이슈만 - if current_user.role == UserRole.USER: + if current_user.role == UserRole.user: issues_query = issues_query.filter(Issue.reporter_id == current_user.id) issues = issues_query.all() @@ -89,7 +89,7 @@ async def get_report_issues( ) # 일반 사용자는 자신의 이슈만 - if current_user.role == UserRole.USER: + if current_user.role == UserRole.user: query = query.filter(Issue.reporter_id == current_user.id) issues = query.order_by(Issue.report_date).all() diff --git a/backend/services/__pycache__/auth_service.cpython-311.pyc b/backend/services/__pycache__/auth_service.cpython-311.pyc index 61217220e0e8ef10f350064ef98cd1e700fed7ce..217c9f5187054522bf6f67e1d6b7cc6eac30de69 100644 GIT binary patch delta 30 kcmdm^vPXq$IWI340}$9GZR9$`%bJ*yo0+%yHg6&e0DoTy)&Kwi delta 30 kcmdm^vPXq$IWI340}wQEZ{#||%j)Rj>*=@oHg6&e0C|-NB>(^b diff --git a/backend/services/__pycache__/file_service.cpython-311.pyc b/backend/services/__pycache__/file_service.cpython-311.pyc index 66d57e2b11bb53ca542390b505b5f9d179cb8b36..38557d6a42a7d2ccbfc0680f76368654c38b50de 100644 GIT binary patch delta 1204 zcmZ8gO>7%Q6rTODcWv+5iEF3X+750SmuyLqKm;X`8k3;dq-s!P)2d0aipjW%o%o0G zrlFQi$^pqj0ScpvsHMUZC4bs|11(kS7AbMRRfZA_C~_q><3Ey$Uv4#c*arTE>tahR{+%0OD-N2xAKH#K zb{9ZNJ9$tGq`L9n-0@Lo#{w--evdtDhWOj>@g-h6-SGe*uQwsi;S2(tEf;JW^nqU{~hR*7-EuQR@%(gH9x{CZ=aF30GQ$TyM zV+UXWAYVz)Mh`8}jpHW8V<`#m2+$G*u!Yu`HSjh!2i6$12j4bLt*e~DN5Yd6276(` zU}g=DEL5)fgd{?Ar)_ndA~#XZCq!OV8G}#9^UDP(Fc(M%VBnR&Odx77SDSQq1^(Go zZc$~MHw6;aM=S<^es(I{yhz|{Dr*RDt;Sc9+6_a@3OOyAP8JgQdGi^@hhyduKcCLR zgu!JJYX+Yulp!o+GuIPHGfcU7UQ6IF%pXXG_09Og@*>KvW*{CG!p=#OyNk^HU*w+M z1pB;-KNQ|r-xq<@y)mXML%NNQKa{@Dl*;gSpn7H}TT?=HB~(7&aJV)Ko6$XoUw8N$ zy}r-oJ96b(t@mub_v|fGL+W`fdG{pm=EaAJA5)J)d*cy(JhJWn&hw4uYhTs3gX-#- zPR8Zeb;(C7iHl|aokSa%JUr?4z~ z_<8- zKh5o0o#B3Nx8FoC3O*RKe917U@UPaFM_(Xkg6P{t^f?=5lt^dEVbY6&#)wjR6r^~Z zvX*PBD=SxXacv1r;48MLOw+`A?!mfEW?l+VFZ!kovvwnw%q${$Ezxt-!p%-!Kqk5z xq^ecU(IWC1LiE?5Y5cR~V`lK5(z~ahVO*8yJ|Lg%%R39zSPTAN&Et<{^rbkcXb%R7i`v>h$JGjH{Yy+5 z6C*Mh6NpI@6-nTL;bQb;0vgY0C1dNi{D@b1a#Q_?AJC ztV#xmq(-$6QSBT@M8qp1wJu90dd7r73()pdcmO;AxpJbm?=Pduc!%I@5OU%zQ5C{K zEu##~fSYUtWT4lIhlRjdolPe%O-M>aNhqo^q#RemIvhE%L8tl+JG{)s9g((AQk@N* z7(DHTx*?U;qKW9ln9c=5fx(SoQuH#qc=h^dJgSYO29jg9*ao-bqv8RJa504vqnggg zQ<*V@pNkK9)Ji3NhCypD0d>T$O+f(efKASgpBT>H{_O2oY%I#YN&9r;C%I)^ZdsF+ zlB^VE?eLlJ7I9PHVEY^>ZwaTrS-b*V|$rmb~3>Qy@ zNi6wJZxxGY&TWCb;Y+=Jy8CJ0RNqU}{Lor`XQ{q3Z`c%o4gWB@4t+Drlfw`C^ZlPC z`BCseaPnrszGTY>*QBnJ)Kx66Z`G1x$@)g6MSn^17bQR5Wr{chu7De?x=jNjyV_B%1Xw`V}dOSXo z9@Q?R9{f&f<_{Bg|MFL<2KshT6+Kb9ppB=a$xDcy9Mnb`Hgx0|;%Vxkq5L~~A$0bd ph7jEabQtfpxL^PWEq7aYLR%rc3CLsq?9zqBb7lB{7{Wg--ajjeM+ diff --git a/backend/services/auth_service.py b/backend/services/auth_service.py index fb762ab..09fedc9 100644 --- a/backend/services/auth_service.py +++ b/backend/services/auth_service.py @@ -63,7 +63,7 @@ def create_admin_user(db: Session): username=admin_username, hashed_password=get_password_hash(admin_password), full_name="관리자", - role=UserRole.ADMIN, + role=UserRole.admin, is_active=True ) db.add(admin_user) diff --git a/backend/services/file_service.py b/backend/services/file_service.py index 06e4776..487adf9 100644 --- a/backend/services/file_service.py +++ b/backend/services/file_service.py @@ -27,20 +27,29 @@ def save_base64_image(base64_string: str) -> Optional[str]: # 이미지 검증 및 형식 확인 image = Image.open(io.BytesIO(image_data)) - format = image.format.lower() if image.format else 'png' - # 파일명 생성 - filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.{format}" + # iPhone의 .mpo 파일이나 기타 형식을 JPEG로 강제 변환 + # RGB 모드로 변환 (RGBA, P 모드 등을 처리) + if image.mode in ('RGBA', 'LA', 'P'): + # 투명도가 있는 이미지는 흰 배경과 합성 + background = Image.new('RGB', image.size, (255, 255, 255)) + if image.mode == 'P': + image = image.convert('RGBA') + background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None) + image = background + elif image.mode != 'RGB': + image = image.convert('RGB') + + # 파일명 생성 (강제로 .jpg) + filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.jpg" filepath = os.path.join(UPLOAD_DIR, filename) # 이미지 저장 (최대 크기 제한) max_size = (1920, 1920) image.thumbnail(max_size, Image.Resampling.LANCZOS) - if format == 'png': - image.save(filepath, 'PNG', optimize=True) - else: - image.save(filepath, 'JPEG', quality=85, optimize=True) + # 항상 JPEG로 저장 + image.save(filepath, 'JPEG', quality=85, optimize=True) # 웹 경로 반환 return f"/uploads/{filename}" diff --git a/docker-compose.yml b/docker-compose.yml index 9328391..ee6d9ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: POSTGRES_USER: mproject POSTGRES_PASSWORD: mproject2024 POSTGRES_DB: mproject + TZ: Asia/Seoul + PGTZ: Asia/Seoul volumes: - postgres_data:/var/lib/postgresql/data - ./backend/migrations:/docker-entrypoint-initdb.d @@ -27,6 +29,7 @@ services: ACCESS_TOKEN_EXPIRE_MINUTES: 10080 # 7 days ADMIN_USERNAME: hyungi ADMIN_PASSWORD: djg3-jj34-X3Q3 + TZ: Asia/Seoul volumes: - ./backend:/app - uploads:/app/uploads diff --git a/frontend/admin.html b/frontend/admin.html index 5a63a56..7c9e994 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -225,7 +225,8 @@ - + + + + + + + + +