UXPlorerλ ν μ€νΈ μ μ λ₯Ό ν μ€νΈν΄ μ£Όκ³ κΈ°λ‘μ λΆμν΄, λ°μ΄ν°λ₯Ό μκ°νν΄ μ£Όλ User Test B2B SaaS μ λλ€.
Deployed website | Frontend Repository | Backend Repository
- Features
- Challenges
- Tech Stacks
- Schedule
- ν
μ€ν°λ ν
μ€νΈ νκ²½μμ UX ν
μ€νΈλ₯Ό μ§ν ν μ μμ΅λλ€.
- νλ©΄ λ Ήν 곡μ κΈ°λ₯μ μ 곡ν¨μΌλ‘μ¨ μ λμ μ§νλ₯Ό λ·λ°μΉ¨ ν΄μ€λλ€.
- ν μ€νΈ μλ£ ν ν μ€ν°κ° μ€λ¬Έ μμ±μ ν μ μλλ‘ μ€λ¬Έ νμ΄μ§λ₯Ό μ 곡ν©λλ€.
- μλΉμ€ μ΄μ©μλ μλΉμ€λ₯Ό ν
μ€νΈνκΈ° μν ν
μ€νΈλ₯Ό λ§λ€ μ μμ΅λλ€.
- ν μ€νΈ μ΄λ¦, ν μ€νΈ μμΈ μ€λͺ , ν μ€νΈ URL, ν μ€ν° μ΄λ©μΌ, λ§κ° λ μ§λ₯Ό μ λ ₯ν©λλ€.
- ν μ€νΈμμ μ§νν λ―Έμ κ³Ό λ―Έμ λ³ μμ μμμκ°μ μ λ ₯ν©λλ€.
- μλΉμ€ μ΄μ©μλ μμ±ν ν
μ€νΈμ λν μΈλΆ μ 보 λ° κ²°κ³Όλ₯Ό λ³Ό μ μμ΅λλ€.
- ν΅ν© κ²°κ³Όμ κ²½μ° λ―Έμ μνλ₯ , λ―Έμ λ³ νκ· μμμκ°, μλΉμ€ λ§μ‘±λ λ° μΆμ² μ§μμ ν΅κ³κ° κ·Έλνλ‘ λνλ©λλ€.
- μ μ λ³ κ²°κ³Όμ κ²½μ° μ μ λ³ μΈλΆ λ―Έμ μνμ¬ν λ° νΌλλ°± μ λ³΄κ° λ³΄μ¬μ§λλ€.
νλ‘ν νμ μ μμ€μ κ³Όμ° μ¬λλ€μ΄ μ΄ μλΉμ€λ₯Ό μ μ¬μ©ν μ μλμ§, νΉμ μ¬μ©νκΈ° μ΄λ ΅κ±°λ 볡μ‘νμ§ μμμ§ κ³ λ―Όμ΄ λ©λλ€. μ΄λ μ¬μ©μ± ν μ€νΈλ₯Ό ν΅ν΄ μ¬μ©μλ€μ΄ μλΉμ€λ₯Ό λͺ©μ μ λ§κ² μ μ¬μ©ν μ μλμ§ μ μ μμ΅λλ€. μ’ λ μ¬μ©μ μΉνμ μΈ μλΉμ€λ‘μ λ°μ μ μν΄ νμμ μΈ μμμ λλ€.
UXPlorerλ μ€νλΌμΈμμ μ§νλλ μ¬μ©μ± ν μ€νΈλ₯Ό μ¨λΌμΈμΌλ‘ μ§νν΄ λ³΄λ©΄ νΈνμ§ μμκΉ νλ λ¨μν νΈκΈ°μ¬μμ μμλμμ΅λλ€.
μ€νλΌμΈμμ μ§νλλ μ¬μ©μ± ν μ€νΈλ λ€μ―κ°μ§μ UX KPIs(Key Performance Indicators)λ₯Ό κΈ°μ€μΌλ‘ ν΅κ³λ₯Ό λ λλ€.
- UX KPIs(Key Performance Indicators)
- Task Success Rate (νμν κ²μ μ°Ύμ ν΄κ²°ν μ¬λμ΄ λͺλͺ μΈμ§)
- Time on Task (νμν κ²μ μΌλ§λ 빨리 μ°Ύμ λλλμ§)
- System Usability Scale : SUS (μ νμ μΌλ§λ λ§μ‘±μ νλμ§) μ§λ¬Έμ§
- Use of Search vs. Navigation (νμν κ²μ μ°ΎκΈ° μν΄ λͺλͺ μ΄ λ΄λΉκ²μ΄μ μ μ¬μ©νκ³ λͺλͺ μ΄ κ²μ κΈ°λ₯μ μΌλμ§)
- User Error Rate(μ€λ₯κ° λͺλ² λ°μνλμ§ μ΄λ»κ² ν΄μ κ·Έλ° μ€λ₯κ° λ΄λμ§)
μ€νλΌμΈμμ μ§νλλ μ¬μ©μ± ν μ€νΈλ₯Ό μ¨λΌμΈμμ μ§νν λ, μ±κ³Ό μ§νλ₯Ό μ΄λ»κ² ν΅κ³νν μ μμμ§ κ³ λ―Όμ΄ λμμ΅λλ€. μ¨λΌμΈ νκ²½μμ μ νν λ°μ΄ν° μμ§κ³Ό λΆμμ΄ μ΄λ €μΈ μ μλ€κ³ μκ°νμ΅λλ€. λν, ν μ€ν°μ μλ°μ μΈ μ°Έμ¬λ₯Ό μ λνλ λ°©λ²λ κ³ λ €ν΄μΌ νμ΅λλ€.
μ λμ μ§νμ μ μ±μ μ§νλ₯Ό κΈ°λ°μΌλ‘ ν ν΅κ³λ μ¬μ©μλ‘ νμ¬κΈ λ³΄λ€ κ°κ΄μ μΈ λ°μ΄ν°λ₯Ό μ 곡ν©λλ€. λμ±μ΄ UXPlorerμ κ²½μ° μ¨λΌμΈ νλ«νΌμ΄λ€ 보λ λκ°μ§ μ§νκ° μ λμ μ΄μμ΅λλ€.
μ¨λΌμΈμμ ꡬν κ°λ₯ν μ§νλ₯Ό μΆλ €λ³΄λ
- λ―Έμ μνλ₯ : λ―Έμ λ³ ν΅κ³Ό μ¬λΆ ν΅κ³ μκ°ν
- λ―Έμ μμμκ°: λ―Έμ λ³ μμμκ° ν΅κ³ μκ°ν
- μλΉμ€ λ§μ‘±λ μ μ: System Usability Scaleμ ν λλ‘ μλΉμ€ λ§μ‘±λ ν΅κ³ μκ°ν
μμ μΈκ°μ§ μ§νλ₯Ό μ λν ν μ μμμ΅λλ€.
κ° ν μ€νΈ λ³λ‘ μλΉμ€ μ΄μ©μ(κΈ°μ )κ° ν μ€ν°λ€μκ² μ 곡ν λ―Έμ μ μμ±ν μ μμ΅λλ€. ν μ€ν°λ λͺ¨λ¬μ μ΄μ©νμ¬ λ³ΈμΈμκ² μ£Όμ΄μ§ λ―Έμ μ μννλλ‘ νμ΅λλ€.
-
λ―Έμ μνλ₯ - μλΉμ€ μ΄μ©μλ ν μ€ν°μ λ―Έμ μ±κ³΅ μ¬λΆλ₯Ό μ΄λ»κ² νλ¨ν μ μμκΉμ? μ΄λ₯Ό μν΄ κ° λ―Έμ μ§μ 건λλ°κΈ° νλͺ©μ λΆμ¬ν΄ μ£Όμ΄ ν μ€ν°κ° μλ°μ μΌλ‘ λ―Έμ μ μννμ§ λͺ»νμλ€λ κ·Όκ±°λ‘ μν μ¬λΆλ₯Ό λ°μ΄ν°ν νμμ΅λλ€. 건λλ°κΈ°μ κ²½μ° μλΉμ€ μ΄μ©μκ° λ―Έμ μ μμ±ν λ λ―Έμ λ³ μμ μμμκ°μ μ€μ ν΄ λκ³ μμ μμμκ°μ΄ κ²½κ³Όνλ©΄ ν μ€ν°κ° 건λλ°κΈ° νλͺ©μ μ νν μ μκ² νμμ΅λλ€. ν μ€ν°κ° λ―Έμ μ 건λλ°μμ κ²½μ° ν΄λΉ λ―Έμ μ μ€ν¨λ‘ κ°μ£Όλ©λλ€.
-
λ―Έμ μμμκ° - κ° λ―Έμ λ³λ‘ ν μ€ν°κ° λ―Έμ μ μλ£ν μκ°μ μ μ μλ€λ©΄, κ·Έλ¦¬κ³ μλΉμ€ μ΄μ©μκ° μλΉμ€ κΈ°ν μ ν΄λΉ κΈ°λ₯μ μλ£ν κ±°λΌλ μμ μμμκ°μ λν μ λ³΄κ° μλ€λ©΄ λ―Έμ μμμκ°μ ν΅κ³ν νμ¬ μ λμ μ§νλ‘ μΌμ μ μμ΅λλ€. μ΄ λ λ―Έμ μμμκ°μ λ°μ΄ν°λ μ±κ³΅ν λ―Έμ μ νμ νμ¬ ν΅κ³μ μ¬μ©ν©λλ€. λ°λΌμ ν μ€ν°κ° 건λλ°κΈ° νλͺ©μ μ ννμμ κ²½μ°μ λ°μ΄ν°λ 무μλ©λλ€.
-
μλΉμ€ λ§μ‘±λ μ μ - μ€λ¬Έμ§ μμ±μ ν΅νμ¬ ν μ€ν°λ μλΉμ€ μ΄μ©μ λν λ§μ‘±λλ₯Ό νκ°ν μ μκ³ μ΄λ μμΉν μν¬ μ μμΌλ©° λΉμ°ν μλΉμ€ μ΄μ©μμκ² μ 곡λ©λλ€. μλΉμ€ μ΄μ©μλ μμΉνλ ν μ€ν°λ³ λ§μ‘±λλ₯Ό κ·Έλνν νμ¬ μκ°μ μΌλ‘ λΉκ΅ ν μ μμ΅λλ€.
-
ν μ€ν°μ μ κ·Ήμ μΈ μ°Έμ¬ - ν μ€ν°λ λ―Έμ μν μ λ―Έμ μ κ΄νμ¬ νΌλλ°±μ μμ λ‘κ² λ¨κΈΈ μ μμ΅λλ€. μ΄ νΌλλ°±μ μλΉμ€ μ΄μ©μμκ² μ 곡λλ©° ν μ€ν°μ μ견μ μ κ·Ή λ°μν μ μκ² λ©λλ€.
-
NPS μ μ - NPS μ μλ κΈ°μ μ΄λ μ νμ κ³ κ° μΆ©μ±λμ λ§μ‘±λλ₯Ό κ°λ¨νλ©΄μλ ν¨κ³Όμ μΌλ‘ μΈ‘μ νλ μ§νμ λλ€. μ΄ μ μλ κ³ κ°μ΄ μ νμ΄λ μλΉμ€λ₯Ό λ€λ₯Έ μ¬λμκ² μΆμ²ν κ°λ₯μ±μ 0λΆν° 10κΉμ§μ μ²λλ‘ λνλ΄λ©°, μ΄λ₯Ό ν΅ν΄ μΆμ²μ, μ€λ¦½μ, λΉνμμ μΈ κ·Έλ£ΉμΌλ‘ λΆλ₯ν©λλ€.
μ΄λ¬ν μ κ·Ό λ°©λ²μ ν΅ν΄ UXPlorerλ μ¨λΌμΈμμμ μ¬μ©μ± ν μ€νΈλ₯Ό λ³΄λ€ ν¨κ³Όμ μΌλ‘ μνν μ μκ² λμκ³ , μ¬μ©μ κ²½νμ λμ± ν₯μμν¬ μ μλ μ€μν λ°μ΄ν°λ₯Ό μμ§ν μ μμμ΅λλ€.
UXPlorer μμ μ¬μ©μμ ν μ€ν°μ μν μ λͺ ννκ² κ΅¬λΆνλ κ²μ΄ νμνμ΅λλ€. μ΄μ λ λ€μκ³Ό κ°μ΅λλ€.
- UXPlorerμ λ°μ΄ν° 보μμ κ°ννκΈ° μν΄ μ¬μ©μλ λ°μ΄ν°λ₯Ό μμ±, μμ , μμ ν μ μλ κΆνμ κ°μ§μ§λ§, ν μ€ν°λ μ£Όμ΄μ§ ν μ€νΈμ νμν μ 보λ§μ μ΄λν μ μμ΄μΌ ν©λλ€.
- 무λΆλ³ν λ°μ΄ν° μ κ·Όμ λ°©μ§νκ³ , ν μ€ν°λ μμ μκ² ν λΉλ ν μ€νΈμλ§ μ κ·Όν μ μλλ‘ μ νν΄μΌ ν©λλ€.
- μ¬μ©μλ ν μ€νΈλ₯Ό κ΄λ¦¬νκ³ λΆμνλ λ° νμν λ€μ 볡μ‘ν μΈν°νμ΄μ€λ₯Ό μ¬μ©νκ³ , ν μ€ν°λ κ°λ¨νκ³ μ§κ΄μ μΈ μΈν°νμ΄μ€λ₯Ό μ¬μ©ν΄μΌ ν©λλ€.
λ‘κ·ΈμΈ λ°©μμ λ¬λ¦¬ μ μ©νμ¬ μ¬μ©μμ ν μ€ν°λ₯Ό ꡬλΆνμμ΅λλ€.
-
μ¬μ©μ λ‘κ·ΈμΈ: μ¬μ©μλ Google Firebaseλ₯Ό μ¬μ©νμ¬ μμ€ν μ λ‘κ·ΈμΈνλλ‘ μ€κ³νμ΅λλ€. μ΄ λ°©μμ μ¬μ©μλ₯Ό μλ³νκ³ μΈμ¦νλ λ° μ¬μ©λλ©°, μ¬μ©μκ° μμ€ν μμ ν μ€νΈλ₯Ό μμ±νκ³ κ΄λ¦¬ν μ μκ² ν©λλ€.
-
ν μ€ν° λ‘κ·ΈμΈ: ν μ€ν°λ μμ±λ ν μ€νΈμ
titleμ κΈ°λ°μΌλ‘ ν κ³ μ ν ν μ€ν° IDμ λΉλ°λ²νΈλ₯Ό μ΄λ©μΌλ‘ λΆμ¬λ°λλ‘ μ€κ³νμ΅λλ€. ν μ€ν°λ μ΄ IDμ λΉλ°λ²νΈλ₯Ό μ¬μ©νμ¬ λ‘κ·ΈμΈνλ©°, μ΄λ ν μ€ν°κ° νΉμ ν μ€νΈμλ§ μ κ·Όν μ μλλ‘ μ νν©λλ€. λΉλ°λ²νΈλ 보μμ μν΄ bcryptλ₯Ό μ¬μ©νμ¬ ν΄μλμ΄ λ°μ΄ν°λ² μ΄μ€μ μ μ₯λ©λλ€.const testerPassword = `${Math.random().toString(36).substring(2, 8)}`; const hashedTesterPassword = await bcrypt.hash(testerPassword, saltRounds);
νμ§λ§ ν
μ€νΈμ titleμ΄ μ€λ³΅λ κ²½μ° λμΌν ν
μ€ν° IDκ° μμ±λλ λ¬Έμ κ° μμμ΅λλ€.
μ΄ λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄, ν μ€νΈ μμ± μ μλ²μ λ°μ΄ν°λ² μ΄μ€μμ μ΄λ―Έ μ‘΄μ¬νλ ν μ€νΈ μ λͺ©μ κ²μ¬νμ¬ μ€λ³΅μ λ°©μ§ν©λλ€. μ΄ λ°©λ²μ ν΅ν΄ λ°μ΄ν°μ μΌκ΄μ±μ μ μ§νκ³ ν μ€ν° IDμ κ³ μ μ±μ ν보ν μ μμμ΅λλ€.
const existingTest = await Test.findOne({ title: testName });
if (existingTest) {
return res.status(400).json({ error: "Same Title already existed" });
}μ΄ μ κ·Ό λ°©λ²μ ν΅ν΄ λ°μ΄ν° 보μμ κ°ννκ³ , μ¬μ©μμ ν μ€ν°μ μν μ λͺ ννκ² κ΅¬λΆν μ μμμ΅λλ€. μ΄λ₯Ό ν΅ν΄ 무λΆλ³ν λ°μ΄ν° μ κ·Όμ λ°©μ§νκ³ , κ° μ¬μ©μκ° μμ μ μν μ λ§λ μΈν°νμ΄μ€λ₯Ό ν¨μ¨μ μΌλ‘ μ¬μ©ν μ μκ² λμμ΅λλ€.
UXPlorerμ κ΄κ±΄μ μΌλ§λ μ¬λ―Έμκ³ μ μ©ν λ°μ΄ν°λ₯Ό λͺ¨μ μ μλλμμ΅λλ€. μ΄λ¬ν λ°μ΄ν°λ μ¬μ©μμκ² λμ± λμμ΄ λλ μμλΌ μκ°νμ΅λλ€. λ°λΌμ κ° ν μ€νΈ νκ²½μ λ 립μ μΌλ‘ κ΄λ¦¬νλ©΄μλ μ μ©ν λ°μ΄ν°λ₯Ό μμ§νλ κ²μ΄ μ€μνμ΅λλ€.
μ΄λ₯Ό μν΄ MongoDBμ μ€ν€λ§ μ€κ³ μ Embedded λ°©μκ³Ό Reference λ°©μμ λΉκ΅ν΄ λ³Ό νμκ° μμμ΅λλ€.
MongoDB 곡μλ¬Έμμ λ°λ₯΄λ©΄ Embedded λ°©μκ³Ό Reference λ°©μμ λ€μκ³Ό κ°μ μ°¨μ΄μ μ΄ μμ΅λλ€.
| λ°©μ | μ₯μ | λ¨μ |
|---|---|---|
| Embedded λ°©μ | Document λ΄μ κ΄λ ¨ λ°μ΄ν°κ° μ§μ ν¬ν¨λλ―λ‘ μ‘°ν μλκ° λΉ λ¦ λλ€. | λ°μ΄ν° μ λ°μ΄νΈκ° 볡μ‘νκ³ , μ€λ³΅μ΄ λ°μν μ μμ΅λλ€. |
| Reference λ°©μ | Document κ°μ κ΄κ³λ₯Ό ID μ°Έμ‘°λ₯Ό ν΅ν΄ λ§ΊμΌλ―λ‘, λ°μ΄ν° μ κ·νκ° μ©μ΄νκ³ , μ λ°μ΄νΈκ° κ°λ¨ν©λλ€. | μ‘°ν μ μΆκ°μ μΈ μ‘°μΈμ΄ νμνλ―λ‘ μλκ° λ€μ λ릴 μ μμ΅λλ€. |
κ²°κ³Όμ μΌλ‘ λ°μ΄ν° μ κ·νκ° μ©μ΄νκ³ μ λ°μ΄νΈκ° κ°λ¨νκΈ° λλ¬Έμ ν μ€νΈ, ν μ€ν°, κ·Έλ¦¬κ³ λ―Έμ λ°μ΄ν°λ MongoDBμ Reference λ°©μμΌλ‘ μ€κ³νμμ΅λλ€. μ΄ λ°©μμ κ° ν μ€νΈ νκ²½μ λ 립μ μΌλ‘ κ΄λ¦¬ν μ μκ² ν©λλ€.
-
ν μ€νΈ λ° λ―Έμ κ΄λ¦¬: κ° ν μ€νΈλ μ¬λ¬ λ―Έμ μ ν¬ν¨νκ³ , μ΄ λ―Έμ λ€μ Mission Collectionμ λ 립μ μΌλ‘ μ μ₯λ©λλ€. κ° ν μ€ν°λ Tester Collectionμ μ μ₯λλ©°, κ° ν μ€ν° μ€ν€λ§λ μνν΄μΌ ν λ―Έμ μ ID λ°°μ΄μ μ°Έμ‘°ν©λλ€.
const missionPromises = missions.map(async (mission, index) => { const newMission = await Mission.create({ description: mission.description, expectedDuration: mission.expectedDuration, order: index, completedBy: testerIds.map((testerId) => ({ tester: testerId })), }); return newMission._id; });
-
λ―Έμ μ°Έμ‘° μ λ°μ΄νΈ: ν μ€ν° λ° ν μ€νΈ λ¬Έμλ λ―Έμ IDλ₯Ό μ°Έμ‘°νμ¬ κ° ν μ€ν°μ ν μ€νΈκ° κ΄λ ¨ λ―Έμ μ μΆμ ν μ μλλ‘ ν©λλ€. μ΄λ ν μ€ν°κ° μμ μκ² ν λΉλ λ―Έμ λ§μ λ³Ό μ μλλ‘ νλ©°, κ° ν μ€νΈ νκ²½μ λ 립μ μΌλ‘ μ μ§ν©λλ€.
await Tester.findByIdAndUpdate(testerId, { $push: { missions: { $each: missionIds } }, });
μ΄ μ κ·Ό λ°©λ²μ ν΅ν΄ κ° ν μ€νΈ νκ²½μ λ 립μ μΌλ‘ κ΄λ¦¬νλ©΄μλ μ μ©ν λ°μ΄ν°λ₯Ό μμ§ν μ μμμ΅λλ€. μ΄λ μ¬μ©μμκ² λμ± λμμ΄ λλ μμλ‘ μμ©ν μ μμμ΅λλ€.
ν μ€ν°λ μ΄λ©μΌλ‘ λΆμ¬λ°μ ν μ€νΈ ID λ° λΉλ°λ²νΈλ₯Ό μ¬μ©νμ¬ ν μ€νΈ νκ²½μ μ§μ ν μ μκ² μ€κ³νμ΅λλ€. λν ν μ€ν° λ‘κ·ΈμΈμ μ°μ»΄ λͺ¨λ¬μ νμ©νμ¬ νμ¬ μ§νλ ν μ€νΈμ λν μ€λͺ μ ν΄μ£Όμμ΅λλ€.
ν μ€νΈ μνμ λͺ¨λ¬μ°½μ μν΄ λ―Έμ λ³λ‘ μ§νλ©λλ€. ν μ€ν°κ° νμ¬ λ―Έμ μ μλ£ν ν λ€μ λ―Έμ μΌλ‘ μνν μ΄λν μ μλλ‘ μ€κ³κ° νμνμ΅λλ€.
μ μ μνλ‘ λ―Έμ μΈλ±μ€λ₯Ό κ΄λ¦¬νλ κ²μ μ¬λ¬ μ»΄ν¬λνΈμμ λ―Έμ λ°μ΄ν°λ₯Ό 곡μ νκ³ μ λ°μ΄νΈν μ μκ² ν΄μ€λλ€. μ΄λ₯Ό ν΅ν΄ κ° λ―Έμ μ μνλ₯Ό μ½κ² μΆμ νκ³ , λ―Έμ μλ£ ν λ€μ λ―Έμ μΌλ‘μ μνν μ΄λμ μ€κ³ν μ μμμ΅λλ€.
Redux, Justand λ± μ€ν μ΄ κΈ°λ°μ μνκ΄λ¦¬ λΌμ΄λΈλ¬λ¦¬λ₯Ό μ΅μ΄ μκ°νμΌλ, μλ² μνλ₯Ό react-queryκ° κ΄λ¦¬ν΄μ£Όκ³ μκΈ°μ ν΄λΌμ΄μΈνΈμ μνλ λ³΄λ€ κ°κ²°ν λμμΈ ν¨ν΄μΌλ‘ κ΄λ¦¬νλκ² μ’κ² λ€κ³ νλ¨ νμ΅λλ€.
μ΄μ λ°λΌ atomμΌλ‘ μνλ₯Ό κ΄λ¦¬νλ Jotaiλ₯Ό μ ν νμΌλ©°, μ»΄ν¬λνΈ λ³λ‘ Jotaiμ μ½κΈ° μ μ© νΉμ μ°κΈ° μ μ© atomμ νμ©νμ¬ μνλ₯Ό κ΄λ¦¬νμμ΅λλ€.
1. λ―Έμ μΈλ±μ€ κ΄λ¦¬: λ―Έμ λ€μ ID λ°°μ΄μ Jotai κΈ°λ° μ μ μν(testerMissionsDataAtom)μμ κ΄λ¦¬νμ¬, νμ¬ λ―Έμ μ μΈλ±μ€λ₯Ό μ½κ² μ°Ύμ μ μλλ‘ νμ΅λλ€. μ΄ λ°°μ΄μ μ¬μ©νμ¬, κ° λ―Έμ μ μλ£ ν λ€μ λ―Έμ μΌλ‘μ λΌμ°ν κ²½λ‘λ₯Ό λμ μΌλ‘ μμ±ν μ μμμ΅λλ€.
λ―Έμ μΈλ±μ€ κ΄λ¦¬ μμ μ½λ
const missionIds = useAtomValue(testerMissionsDataAtom);
const currentMissionIndex = missionIds.findIndex((id) => id === missionId);2. μλ λΌμ°ν λ‘μ§: navigateToNextMission ν¨μλ νμ¬ λ―Έμ μΈλ±μ€λ₯Ό κΈ°λ°μΌλ‘ λ€μ λ―Έμ μ μΈλ±μ€λ₯Ό κ³μ°ν©λλ€. λ§μ½ λ€μ λ―Έμ μΈλ±μ€κ° λ°°μ΄ κΈΈμ΄ λ΄μ μλ€λ©΄, ν΄λΉ λ―Έμ IDλ‘ λΌμ°ν μ μ λ°μ΄νΈν©λλ€. λͺ¨λ λ―Έμ μ μλ£νμ κ²½μ°, ν μ€ν°λ₯Ό μ€λ¬Έ μ‘°μ¬ νμ΄μ§λ‘ 리λλ μ ν©λλ€.
3. νλ©΄ λ Ήν μ€μ§: κ° λ―Έμ μ μλ£ μ, νλ©΄ λ Ήνλ₯Ό μ€μ§(screenRecorder.stop())νκ³ μλ²μ λ°μ΄ν°λ₯Ό μ μ‘νλ λ‘μ§μ μνν©λλ€.
μλ λΌμ°ν λ° νλ©΄ λ Ήν μ€μ§ μμ μ½λ
function navigateToNextMission() {
const nextMissionIndex = currentMissionIndex + 1;
if (nextMissionIndex < missionIds.length) {
const nextMissionId = missionIds[nextMissionIndex];
navigate(`/tester/${testerId}/mission/${nextMissionId}`);
} else {
screenRecorder.stop(); // νλ©΄ λ
Ήν μ€μ§
navigate(`/tester/${testerId}/survey`);
}
}μ΄ μ κ·Ό λ°©λ²μ ν΅ν΄ ν μ€ν°κ° λ―Έμ μ μννκ² μννκ³ λ€μ λ―Έμ μΌλ‘ μ΄λν μ μλ ν¨μ¨μ μΈ μμ€ν μ ꡬμΆν μ μμμ΅λλ€. λν, μ μ λΌμ΄λΈλ¬λ¦¬μΈ Jotaiλ₯Ό μ΄μ©, κ° λ―Έμ μ μνλ₯Ό ν¨κ³Όμ μΌλ‘ κ΄λ¦¬ν μ μκ² λμμ΅λλ€.
ν μ€ν°μ νΌλλ°±μ ν¨κ³Όμ μΌλ‘ μμ§νκ³ μμ€ν κ°μ μ νμ©νλ κ²μ΄ νμνμ΅λλ€.
ν μ€ν°λ λ―Έμ μν μ λ―Έμ μ κ΄νμ¬ νΌλλ°±μ μμ λ‘κ² λ¨κΈΈ μ μλλ‘ μ€κ³νμμ΅λλ€. μ΄ νΌλλ°±μ μ¬μ©μμκ² μ 곡λλ©° ν μ€ν°μ μ견μ μ κ·Ή λ°μν μ μκ² λ©λλ€.
ν μ€ν°μ νΌλλ°±μ μμ§νκ³ μ΄λ₯Ό μμ€ν κ°μ μ νμ©νλ μ μ°¨λ λ€μκ³Ό κ°μ΅λλ€:
1. νΌλλ°± μ
λ ₯ μΈν°νμ΄μ€: ν
μ€ν°λ FeedbackTextarea μ»΄ν¬λνΈλ₯Ό ν΅ν΄ νΌλλ°±μ μ
λ ₯ν μ μμ΅λλ€. νΌλλ°±μ λ‘컬 μν(feedback)μ μ μ₯λλ©°, μ μ₯ λ²νΌ ν΄λ¦ μ μλ²λ‘ μ μ‘ μ€λΉκ° μλ£λ©λλ€.
νΌλλ°± μ λ ₯ μΈν°νμ΄μ€ μμ μ½λ
// νΌλλ°± μ
λ ₯ μΈν°νμ΄μ€
const [feedback, setFeedback] = useState(""); // λ‘컬 μν
function handleFeedbackChange(event) {
setFeedback(event.target.value); // μ
λ ₯ν νΌλλ°±μ μνμ μ μ₯
}
function handleFeedbackSubmit() {
setIsFeedbackOpen(false); // νΌλλ°± μ
λ ₯ μΈν°νμ΄μ€ λ«κΈ°
}2. μλ²μ νΌλλ°± μ μ‘: νΌλλ°±κ³Ό κ΄λ ¨λ λ°μ΄ν°λ handleNextMission λ° handleSkipMission ν¨μλ₯Ό ν΅ν΄ μ²λ¦¬λ©λλ€. μ΄λ€ ν¨μλ λ―Έμ μλ£ λλ 건λλ°κΈ° λ°μ΄ν°μ ν¨κ» νΌλλ°±μ μλ²λ‘ μ μ‘ν©λλ€. μ μ‘ μ±κ³΅ ν, μ¬μ©μλ μλμΌλ‘ λ€μ λ―Έμ μΌλ‘ μ΄λνκ±°λ μ€λ¬Έ μ‘°μ¬λ₯Ό μνν μ μμ΅λλ€.
μλ²μ νΌλλ°± μ μ‘ μμ μ½λ
// μλ²μ νΌλλ°± μ μ‘
function handleNextMission() {
const missionCompletionData = {
completed: true,
createdAt: new Date(new Date() - missionTime * 1000),
completedAt: new Date(),
duration: missionTime,
feedback,
};
setFeedback(""); // νΌλλ°± μν μ΄κΈ°ν
updateMissionData(missionCompletionData, {
onSuccess: () => {
navigateToNextMission(); // μ μ‘ μ±κ³΅ μ λ€μ λ―Έμ
μΌλ‘ μ΄λ
},
});
}μ΄ μ κ·Ό λ°©λ²μ ν΅ν΄ ν
μ€ν°μ νΌλλ°±μ ν¨κ³Όμ μΌλ‘ μμ§νκ³ μ΄λ₯Ό μμ€ν
κ°μ μ λ°μν μ μμμ΅λλ€. μ΄λ μ¬μ©μ κ²½νμ λμ± ν₯μμν€λ λ° κΈ°μ¬νμμ΅λλ€.
μ€νλΌμΈμμμ λ€μ―κ°μ§μ μ±κ³Ό μ§νλ₯Ό κΈ°μ€μΌλ‘ μ¨λΌμΈμμ ꡬνν΄μΌν λ―Έμ μνλ₯ , λ―Έμ μμμκ°, κ·Έλ¦¬κ³ μλΉμ€ λ§μ‘±λ μ μλ₯Ό ꡬννμμ΅λλ€. νμ§λ§ μ΄ μ§νλ€μ μ΄λκΉμ§λ ν μ€ν°μ μ¬λμ λ§‘κΈ°κ² λμ΄ μ¨λΌμΈμμμ μλ²½ν λ°μ΄ν°λ₯Ό μΆμΆνλ λ°μλ νκ³κ° μλ€κ³ μκ°νμ΅λλ€. λ°λΌμ UXPlorerλ νλ©΄λ Ήν κΈ°λ₯μ μ 곡νμ¬ μ¨λΌμΈ μμμ ν μ€ν°λ₯Ό Observation νλ μ©λλ‘ μ¬μ©νκ³ μλΉμ€ μ΄μ©μλ ν μ€ν°μ νλ©΄ λ Ήνλ³Έμ μΆκ°μ μΈ μ λμ μ§νλ₯Ό 보λλ°μ μ¬μ©ν μ μλλ‘ μ€κ³κ° νμνμ΅λλ€.
ν μ€νΈ νλ©΄ λ Ήν κΈ°λ₯μ ꡬννκΈ° μν΄μλ μμ μ μΈ λ―Έλμ΄ μ€νΈλ¦Ό μμ§κ³Ό μ μ₯ λ°©μμ΄ νμνμ΅λλ€. λν νλ©΄ λ Ήν λ°μ΄ν°λ₯Ό ν¨μ¨μ μΌλ‘ μλ²μ μ μ‘νκ³ μ μ₯ν μ μλ λ°©λ²μ μ°ΎμμΌ νμ΅λλ€.
ν¬λ‘¬ λΈλΌμ°μ κΈ°μ€μΌλ‘ νΉμ νμ νλ©΄ λ Ήν κΈ°λ₯μ ꡬννκΈ° μν΄μ MediaDevice μΈν°νμ΄μ€μ getMediaDisplay λ©μλλ₯Ό μ¬μ©νμμ΅λλ€. MediaDevice μΈν°νμ΄μ€λ λ―Έλμ΄ λ°μ΄ν°λ₯Ό μ 곡νλ λͺ¨λ νλμ¨μ΄λ‘ μ κ·Όν μ μλ λ°©λ²μ μ 곡ν΄μ€λλ€.
1. νλ©΄ λ Ήν μμ
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
preferCurrentTab: true,
});ν μ€ν°μ νλ©΄ λ Ήνλ₯Ό μμνλ μ½λμ λλ€. getDisplayMedia() λ©μλλ μ¬μ©μμ μ 체 νλ©΄ λλ νΉμ νμ λ―Έλμ΄ μ€νΈλ¦Όμ λ°νν΄μ€λλ€. μ¬κΈ°μλ preferCurrentTab: true μ΅μ μ μ¬μ©ν΄ νμ¬ νμ± νλ§ λ Ήννλλ‘ μ€μ νμ΅λλ€.
2. λ―Έλμ΄ λ°μ΄ν°μ μμ§
const recorder = new MediaRecorder(stream, { mimeType: "video/webm" });
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);MediaRecorder κ°μ²΄λ₯Ό μ΄μ©νμ¬ λ Ήνλ μ€νΈλ¦Όμ μ²λ¦¬νλλ‘ κ΅¬ννμμ΅λλ€. μ΄ κ°μ²΄λ μ€μ λ mimeTypeμ λ°λΌ λ°μ΄ν°λ₯Ό μΈμ½λ©νκ³ , ondataavailable μ΄λ²€νΈλ₯Ό ν΅ν΄ λ Ήν λ°μ΄ν°λ₯Ό chunks λ°°μ΄μ μ μ₯ν©λλ€.
3. λ Ήν μ’ λ£ λ° λ°μ΄ν° μ²λ¦¬
recorder.onstop = async () => {
const blob = new Blob(chunks, { type: "video/mp4" });
const formData = new FormData();
formData.append("video", blob, `${fileName}.mp4`);
};λ Ήνκ° μ’ λ£λλ©΄ (recorder.onstop μ΄λ²€νΈ), chunks λ°°μ΄μ μ μ₯λ λΉλμ€ λ°μ΄ν°λ Blob κ°μ²΄λ‘ λ³νλλ©°, μ΄λ μλ²λ‘ μ μ‘ μ€λΉλ₯Ό μν΄ FormData κ°μ²΄μ μΆκ°λ©λλ€.
4. 리μμ€ μ 리
stream.getTracks().forEach((track) => track.stop());λ Ήνκ° μλ£λ νμλ μ¬μ©λ λͺ¨λ λ―Έλμ΄ νΈλμ μ’ λ£ν©λλ€.
μ΄ μ κ·Ό λ°©λ²μ ν΅ν΄ μμ μ μ΄κ³ ν¨μ¨μ μΈ νλ©΄ λ
Ήν κΈ°λ₯μ ꡬνν μ μμμΌλ©°, λ
Ήν λ°μ΄ν°λ₯Ό μμ μ μΌλ‘ μλ²μ μ μ₯ν μ μμμ΅λλ€. μ΄λ₯Ό ν΅ν΄ μ¨λΌμΈ μμμ λ μ νν μ¬μ©μ± ν
μ€νΈ λ°μ΄ν°λ₯Ό μμ§ν μ μκ² λκ³ κ·Έ κ·Όκ±°λ₯Ό λ·λ°μΉ¨ ν μ μμμ΅λλ€.
νλ©΄ λ Ήν κΈ°λ₯ ꡬν μ λ―Έλμ΄ νμΌ νμμ μ νμ λ€μν νλ«νΌμμ νΈνμ±μ 보μ₯νλ λ° μ€μν μν μ νλ€κ³ μκ°νμ΅λλ€. λνμ μΌλ‘ MP4μ WebMμ΄ μλλ°, λ νμΌ νμμ κΈ°μ μ μ΄ν΄κ° νμνμ΅λλ€.
1. WebMκ³Ό MP4μ λΉκ΅
- WebM: Googleμ μν΄ κ°λ°λ μ€ν μμ€ λΉλμ€ ν¬λ§·μΌλ‘, μ£Όλ‘ VP8 λ° VP9 λΉλμ€ μ½λ±κ³Ό Vorbis λλ Opus μ€λμ€ μ½λ±μ μ¬μ©ν©λλ€. WebMμ νΉν HTML5 μΉ λΉλμ€ νλ μ΄μ΄μμ λ리 μ§μλλ©°, μΉ κΈ°λ° μ€νΈλ¦¬λ°μ μ΅μ νλμ΄ μμ΅λλ€.
- MP4: H.264/AVC λλ H.265/HEVC λΉλμ€ μ½λ±κ³Ό AAC μ€λμ€ μ½λ±μ μ¬μ©νλ λ³΄λ€ λ²μ©μ μΈ λ―Έλμ΄ μ»¨ν μ΄λ ν¬λ§·μ λλ€. MP4λ κ±°μ λͺ¨λ μμ€ν λ° μ₯μΉμμ λ리 μ§μλλ©°, κ³ νμ§ μ€νΈλ¦¬λ° λ° μ μ₯ μ΅μ μ μ 곡ν©λλ€.
2. νλ«νΌ λ° λλ°μ΄μ€ νΈνμ±
-
λΈλΌμ°μ νΈνμ±: WebMμ μ£Όλ‘ Firefox, Chrome λ° Operaμ κ°μ μΉ λΈλΌμ°μ μμ λ리 μ§μλμ§λ§, Safariμ μΌλΆ λͺ¨λ°μΌ λΈλΌμ°μ μμλ μ νμ μΈ μ§μμ λ°μ΅λλ€. λ°λ©΄, MP4 (νΉν H.264 μ½λ± μ¬μ© μ)λ κ±°μ λͺ¨λ μ£Όμ λ°μ€ν¬ν λ° λͺ¨λ°μΌ λΈλΌμ°μ μμ μ§μλ©λλ€.
-
μ μ₯ λ° λ€μ΄λ‘λ κ°λ₯μ±: ν΄λΌμ°λ μ€ν λ¦¬μ§ μλΉμ€ λ° μ½ν μΈ μ μ‘ λ€νΈμν¬μμ MP4 νμΌμ νμ€μ μΈ λΉλμ€ νμμΌλ‘ κ°μ£Όλμ΄ λ€μ΄λ‘λ λ° μ€νΈλ¦¬λ°μ΄ μ©μ΄ν©λλ€. Amazon S3μ κ°μ μλΉμ€μμλ MP4 νμΌμ μ°μ μ μΌλ‘ μ§μνλ©°, μ΄λ WebM νμμ λΉν΄ μ κ·Όμ± λ° νΈνμ± λ©΄μμ ν° μ΄μ μ μ 곡ν©λλ€.
3. κΈ°μ μ ꡬν
μ½λ μμμμ getDisplayMedia λ₯Ό μ¬μ©νμ¬ λ Ήνλ μ€νΈλ¦Όμ video/webm λ‘ μ΄κΈ° μ€μ νμμΌλ, μ΅μ’ μ μΌλ‘λ Blob μμ± μ video/mp4 νμμΌλ‘ λ³ννμ¬ μ μ₯νλλ‘ κ΅¬ννμμ΅λλ€. μ΄λ MP4 νμμ λ²μ©μ±κ³Ό νΈνμ±μ νμ©νκΈ° μν¨μ λλ€. μλ²λ‘ μ μ‘λ ν, MP4 νμΌμ λ€μν μ₯μΉ λ° νλ«νΌμμ λ¬Έμ μμ΄ μ¬μλ μ μμΌλ©°, νμν κ²½μ° λ€λ₯Έ νμμΌλ‘μ μΆκ° λ³ν μμ΄ λ°λ‘ μ¬μ©λ μ μλλ‘ νκΈ° μν¨μ΄μμ΅λλ€.
μ΄ μ κ·Ό λ°©λ²μ ν΅ν΄ λ€μν νλ«νΌμμ νΈνμ±μ΄ 보μ₯λλ λ―Έλμ΄ νμΌ νμμ μ¬μ©νμ¬ μμ μ μΈ νλ©΄ λ Ήν κΈ°λ₯μ μ 곡ν μ μμμ΅λλ€. μ΄λ₯Ό ν΅ν΄ λ λ§μ μ¬μ©μμκ² μΌκ΄λ κ²½νμ μ 곡ν μ μκ² λμμ΅λλ€.
1 μ£Όμ°¨: μμ΄λμ΄ μ μ , POC, λͺ©μ λ° DBμ€ν€λ§ μ€κ³
2 - 3μ£Όμ°¨: ν΄λΌμ΄μΈνΈ ꡬν λ° μλ² κ΅¬ν








