경험 표집, 후속 조사 웨이브, 델파이 라운드
등록 후 몇 시간 또는 몇 주 뒤에 설문 세션을 열고, 이메일, Twilio SMS, Prolific 메시지로 참가자에게 알림을 보내며, 응답 이행률과 응답 지연 시간을 내보냅니다.
v2.13.0의 새로운 기능
schedule: 블록을 사용하면 Potato는 참가자가 처음 방문한 뒤에 짧은 설문 세션을 열고, notifications: 블록을 사용하면 각 세션이 열릴 때 이메일, SMS, Prolific 메시지로 알림을 보냅니다. 설문 연구자는 후속 조사 웨이브를 종단 패널에, 심리학자와 보건 연구자는 경험 표집을 일기 및 기분 연구에, 전문가 패널은 델파이 라운드를 기준이나 지침에 대한 합의 도출에 사용합니다. Potato는 모든 세션에 대해 응답, 알림이 발송된 시각, 참가자가 양식을 처음 건드린 시각, 그리고 세션이 응답됨, 만료됨, 놓침 중 어느 상태인지를 기록합니다.
경험 표집법(ESM)은 참가자에게 같은 짧은 질문을 하루에 여러 번 던져 각 알림 시점의 순간에 대해 묻습니다. 보건 연구자들은 같은 설계를 생태순간평가(EMA)라고 부릅니다. 델파이 기법에서는 전문가 패널이 같은 항목을 여러 라운드에 걸쳐 평가하며, 다시 평가하기 전에 이전 라운드의 익명 패널 응답을 확인합니다.
- 시간대. 각 참가자는 등록 시 시간대를 선택하며, 등록 페이지에는 브라우저의 시간대가 미리 채워집니다.
"09:00"같은 시각은 각 현지 날짜의 참가자 시간대에 맞춰 배치됩니다. - 채널. 메시지는 임의의 SMTP 서버를 통한 이메일, Twilio를 통한 SMS, 또는 참가자의 Prolific 받은편지함으로 발송됩니다. 자격 증명은 환경 변수에서만 읽습니다.
- 연락처. Potato는 동의 질문에 "예"라고 답한 뒤에만 이메일 주소나 전화번호를 요청하며, 주소를 출력 디렉터리 밖에 보관하고 모든 내보내기에서 제외합니다.
등록과 세션 계획
각 참가자는 다음 단계를 한 번 거칩니다.
- 참가자가 동의, 안내, 어노테이션, 설문으로 이루어진 일반 흐름을 완료합니다.
- 흐름이 끝나면 등록 페이지가 시간대를 묻고, 동의 질문에 "예"라고 답한 경우에만 이메일 주소나 휴대전화 번호를 묻습니다. 사전 선별에서 제외되었거나 훈련에 실패했거나 주의력 확인에서 차단된 참가자는 등록되지 않습니다.
- Potato는 그 시점에 참가자의 모든 세션과 모든 알림을 계산하여 계획을 저장합니다.
- 백그라운드 루프가 각 메시지를 예정된 시각에 발송하며, 메시지에는 참가자를 로그인시키고 세션을 여는 재개 링크가 들어 있습니다.
- 연구 링크로 돌아온 참가자는 열려 있는 세션으로 이동하거나, 다음 세션이 참가자 자신의 시간대로 언제 열리는지 보여 주는 상태 페이지로 이동합니다.
계획과 시간대는 등록 시점에 고정됩니다. 나중에 waves를 수정하면 그 이후에 등록하는 참가자에게만 계획이 바뀝니다. 연락을 거부한 참가자도 등록되며 메시지는 받지 않고, 연구 링크로 돌아와 열린 세션을 찾습니다.
구성
아래 구성은 오전 9시부터 오후 9시 사이 무작위 시각에 하루 다섯 번 알림을 보내는 7일간의 경험 표집 연구를 실행합니다. 일반 흐름은 동의 페이지와 사전 조사 항목 하나로 이루어지며, 사전 조사 항목을 마치면 참가자가 등록됩니다.
annotation_task_name: "Daily mood study"
task_dir: .
output_annotation_dir: annotation_output
data_files: [data/intake.jsonl]
item_properties: {id_key: id, text_key: text}
user_config:
allow_all_users: true
phases:
order: [consent, annotation]
consent:
type: consent
file: surveys/consent.json
annotation:
type: annotation
annotation_schemes: # the intake questions, answered once
- annotation_type: likert
name: usual_mood
description: "In general, how would you describe your mood?"
size: 7
min_label: "Very low"
max_label: "Very good"
schedule:
timezone: "America/Detroit" # used when the browser reports no time zone
seed: "daily-mood-2026"
experience_sampling:
title: "How are you right now?"
days: 7
start_day: 1 # first prompts on the day after enrollment
daily_window: "09:00-21:00"
signal: {per_day: 5, min_gap: 90m}
expires_after: 30m
questions:
- {name: mood, annotation_type: likert, size: 7, required: true,
description: "How do you feel right now?",
min_label: "Very bad", max_label: "Very good"}
- {name: company, annotation_type: radio, required: true,
description: "Who are you with?",
labels: ["Alone", "Family", "Friends", "Colleagues", "Strangers"]}
reminders:
- at: open
- after_open: 15m
notifications:
dry_run: true # pilot: render and log every message, send none
base_url: "https://mood-study.example.org"
channels:
- type: log
- type: twilio
account_sid_env: TWILIO_ACCOUNT_SID
auth_token_env: TWILIO_AUTH_TOKEN
from_number: "+12025550143"
contact:
consent: {question: contact_consent, value: "Yes"}두 개의 알림은 프롬프트가 열릴 때 프롬프트를 보내고, 15분 뒤 아직 응답하지 않은 참가자에게 두 번째 메시지를 보냅니다. dry_run: true이면 Potato는 모든 Twilio 메시지를 생성하고 수신자를 지정하며 관리자 페이지에 각 메시지를 나열하지만 하나도 전송하지 않습니다. 시범 운영이 끝나면 dry_run: false로 설정하세요. 무작위 프롬프트 시각은 seed, 참가자 ID, 날짜로부터 추출됩니다. 각 참가자는 독립적인 추출을 받으며, 추출 결과는 나중에 다시 계산할 수 있습니다.
surveys/consent.json의 동의 페이지는 notifications.contact.consent가 지정한 질문을 던집니다.
[
{"name": "study_consent", "annotation_type": "radio",
"description": "I agree to answer short surveys five times a day for seven days.",
"labels": ["I agree", "I do not agree"],
"label_requirement": {"required_label": ["I agree"]}},
{"name": "contact_consent", "annotation_type": "radio",
"description": "May we text you when each survey opens? Your number is kept apart from your answers and deleted when the study ends.",
"labels": ["Yes", "No"],
"label_requirement": {"required": true}}
]시작할 때 Potato는 contact_consent가 단계 페이지에서 질문되는지, 그리고 Yes가 그 질문의 선택지 중 하나인지 확인합니다. YAML 1.1은 따옴표 없는 Yes를 불리언으로 읽고 동의 값이 true 또는 false가 되면 검증에 실패하므로, YAML에서는 "Yes"를 따옴표로 묶으세요. value는 연락을 허용하는 응답의 목록일 수도 있습니다.
경험 표집 옵션
experience_sampling 키 | 기본값 | 의미 |
|---|---|---|
days | 필수 | 표집 일수, 1부터 366까지 |
start_day | 1 | 등록일부터 센 첫 표집일이며, 0은 등록일 당일 |
daily_window | "09:00-21:00" | 프롬프트가 들어가는 현지 시간 범위 |
signal.per_day | signal과 함께 필수 | 하루 무작위 프롬프트 수, 1부터 48까지 |
signal.min_gap | 60m | 두 프롬프트 사이의 최소 간격으로, 최소 expires_after 이상 |
interval.every | 없음 | 시간 범위 시작 시점과 시작 이후 매 간격마다 프롬프트 |
interval.times | 없음 | ["10:00", "14:00", "20:00"]처럼 고정된 현지 시각 |
expires_after | 30m | 각 프롬프트가 열려 있는 시간, 최소 1분 |
questions, questions_file | 하나 필수 | 프롬프트의 질문(아래 세션 질문 절 참조) |
reminders | [{at: open}] | at: open, after_open, before_close. before_open은 거부됩니다 |
title | "Short survey" | 프롬프트 페이지와 메시지에 표시됨 |
intro | 없음 | 질문 위에 표시되는 텍스트 |
연구는 signal(신호 수반형, 무작위 시각) 또는 interval(간격 수반형, 고정 시각) 중 정확히 하나만 사용합니다. interval 블록은 every 또는 times 중 정확히 하나를 받습니다. 기간은 30m, 48h, 7d, 2w로 쓰거나 초 단위 숫자로 씁니다.
일일 시간 범위에 들어맞지 않는 계획은 검증에 실패합니다. 90분 간격의 다섯 프롬프트는 90분짜리 간격 네 개에 마지막 프롬프트가 열려 있는 30분을 더한 6시간 30분의 시간 범위가 필요합니다. 같은 간격의 아홉 프롬프트는 12시간 30분이 필요하므로 12시간 범위에서는 검증에 실패합니다. 시간 범위보다 긴 interval.every와 expires_after보다 가깝게 붙은 고정 시각도 같은 방식으로 실패합니다.
Potato는 매일 해당 현지 날짜의 참가자 시간대에 일일 시간 범위를 배치합니다. 따라서 시계가 바뀌는 날에도 시간 범위는 현지 시각 09:00부터 21:00까지입니다. 무작위 시각은 프롬프트 사이를 최소 min_gap만큼 띄우고 마지막 프롬프트를 시간 범위 안에서 닫는 모든 배치에 걸쳐 균등하게 추출됩니다. 상태 페이지는 그날의 시간 범위를 보여 줄 뿐 다음 무작위 프롬프트의 시각은 절대 보여 주지 않습니다. 등록 시점에 이미 시각이 지난 프롬프트는 제외됩니다.
후속 조사 웨이브
종단 패널은 waves를 사용합니다. 아래의 부분 블록은 위 구성의 schedule을 대체하며 1주 후와 1개월 후 후속 조사를 추가합니다.
schedule:
timezone: "America/Detroit"
waves:
- name: week1
title: "One-week follow-up"
offset: 7d
at: "09:00" # local clock time on day 7
window: 48h
questions:
- {name: changed_view, annotation_type: radio, required: true,
description: "Since last week, has your view changed?",
labels: ["No", "A little", "A lot"]}
reminders:
- before_open: 12h
- at: open
- before_close: 12h
completion_code: "FOLLOWUP-DONE"
- name: month1
offset: 30d
at: "09:00"
window: 72h
questions_file: surveys/followup.jsonat를 쓰면 중간에 시계가 바뀌더라도 각 웨이브는 지정한 날의 현지 시각 09:00에 열리며, 이때 offset은 정수 일수여야 합니다. at이 없으면 offset: 7d는 경과 시간 7일을 뜻하고, 일광 절약 시간제 변경이 있으면 현지 개시 시각이 한 시간 이동합니다. "09:00" 같은 시각은 따옴표로 묶으세요. YAML 1.1은 따옴표 없는 10:30을 숫자 630으로 읽고, Potato는 630을 자정 이후의 분으로 읽습니다.
waves 키 | 기본값 | 의미 |
|---|---|---|
name | 필수 | 내보내기에 쓰이는, 영문자, 숫자, _, - 최대 40자의 짧은 ID |
title | name | 참가자와 메시지에 표시됨 |
offset | 0 | 등록 이후의 시간 |
at | 없음 | 등록 후 offset일째 되는 날의 현지 시각 |
window | 필수 | 웨이브가 열려 있는 시간, 최소 1분 |
questions, questions_file | 하나 필수 | 웨이브의 질문 |
reminders | [{at: open}] | 메시지가 발송되는 시점 |
completion_code | 없음 | 참가자가 웨이브를 제출한 뒤 표시되는 코드이며, 예를 들어 두 번째 Prolific 연구에서 보상을 청구할 때 사용 |
intro | 없음 | 질문 위에 표시되는 텍스트 |
델파이 라운드
델파이 연구는 delphi를 사용합니다. 아래의 부분 블록은 경험 표집 구성의 schedule을 대체하며, data/criteria.json에는 {"id", "text"} 항목의 목록이 들어 있습니다.
schedule:
timezone: "UTC"
delphi:
title: "Guideline criteria"
items_file: data/criteria.json
scale: {min: 1, max: 9, min_label: "Not important", max_label: "Essential"}
comments: true
max_rounds: 3
round_length: 72h
gap: 12h
start: "2026-11-16T09:00:00-05:00"
min_panel: 3
stop:
iqr_at_most: 1
changed_share_below: 0.15
reminders:
- at: open
- before_close: 24h라운드는 패널 전체에 적용됩니다. 1라운드는 start에 열리며, start가 설정되지 않았다면 해당 구성으로 처음 서버를 시작할 때 열리고, Potato는 그 시각을 재시작 후에도 유지합니다. 이후 각 라운드는 이전 라운드가 닫히고 gap이 지난 뒤에 열립니다. 2라운드 중에 등록한 패널리스트는 2라운드에 합류하며, 자신의 평가 없이 1라운드 피드백을 봅니다. 메시지를 중단한 패널리스트도 알림 없이 각 라운드에 참여합니다.
2라운드부터 각 항목에는 이전 라운드에 관한 다음 정보가 표시됩니다.
- 중앙값, 그리고 척도 위에 음영 띠로 표시된 평가의 가운데 절반(Q1부터 Q3까지)이며, 패널리스트 본인의 평가가 표시됩니다.
- 평가 수.
- 다른 패널리스트의 코멘트이며 이름은 표시되지 않고, 패널리스트별로 고정된 순서로 나타납니다. 패널리스트 본인의 코멘트는 다시 보여 주지 않습니다.
min_panel명보다 적은 패널리스트가 평가한 항목에는 요약도 코멘트도 표시되지 않습니다. 이 임계값 덕분에 소규모 패널의 평가를 한 사람에게 거슬러 추적할 수 없습니다. 현재 라운드의 평가는 미리 채워지지 않습니다.
라운드의 시간 범위가 끝나 라운드가 닫힐 때, 또는 관리자가 /admin/schedule에서 라운드를 일찍 닫을 때, Potato는 각 항목의 n, 중앙값, 평균, Q1, Q3, 사분위수 범위(IQR)를 계산합니다. 사분위수는 순서 통계량 사이의 선형 보간을 사용하며, 이는 R이 type 7이라 부르는 방법이자 NumPy의 기본값입니다. 2라운드부터 Potato는 median_change와 changed_share도 계산합니다. median_change 값은 중앙값의 절대 변화량이고, changed_share는 두 라운드 모두에서 해당 항목을 평가하고 평가를 바꾼 패널리스트의 비율입니다. 항목이 설정된 기준 중 하나 이상을 충족하면 해당 항목은 확정된 것으로 봅니다.
stop 키 | 종류 | 확정되는 경우 |
|---|---|---|
iqr_at_most | 합의 | IQR이 해당 값 이하 |
median_change_at_most | 안정성, 2라운드부터 | 이전 라운드 이후 중앙값이 해당 값 이하로 움직임 |
changed_share_below | 안정성, 2라운드부터 | 평가를 바꾼 패널리스트의 비율이 해당 값(0부터 1까지) 미만 |
연구는 모든 항목이 확정된 첫 번째 라운드 이후, 또는 max_rounds 이후에 멈춥니다. stop 블록이 없으면 연구는 max_rounds 라운드를 모두 실행합니다. 중단 사유는 패널리스트의 상태 페이지와 관리자 페이지에 표시됩니다.
delphi 키 | 기본값 | 의미 |
|---|---|---|
items, items_file | 하나 필수 | 인라인 [{id, text}] 또는 JSON, JSONL, YAML 파일 |
scale | {min: 1, max: 9} | 최대 21점의 정수 평가 척도, 선택적으로 min_label과 max_label 포함 |
comments | true | 항목당 최대 2,000자의 선택적 코멘트이며, 이름 없이 패널에 표시됨 |
max_rounds | 3 | 라운드 수의 상한, 1부터 10까지 |
round_length | 필수 | 각 라운드가 열려 있는 시간 |
gap | 0 | 한 라운드가 닫힌 뒤 다음 라운드가 열릴 때까지의 시간 |
start | 첫 서버 시작 | 1라운드가 열리는 시점이며, UTC 오프셋이 붙은 ISO 8601 형식 |
min_panel | 3 | 항목의 요약과 코멘트가 표시되기 위해 필요한 평가 수 |
stop | 없음 | 중단 기준 |
reminders | [{at: open}] | 메시지가 발송되는 시점 |
title | "Delphi round" | 라운드 페이지에 표시되며 뒤에 라운드 번호가 붙음 |
intro | 없음 | 항목 위에 표시되는 텍스트 |
세션 질문
웨이브와 프롬프트의 질문은 어노테이션 스킴처럼 questions 아래에 인라인으로 쓰거나, 구성 파일 기준 상대 경로로 읽는 JSON 또는 YAML questions_file에 씁니다. 각 질문은 name, annotation_type, description, 그리고 required: true 또는 label_requirement: {required: true} 중 하나를 받습니다. 여섯 가지 질문 유형은 다음 키를 받습니다.
| 유형 | 키와 기본값 |
|---|---|
radio | labels(최소 두 개), horizontal |
multiselect | labels(최소 두 개) |
likert | size(2부터 11까지), min_label, max_label |
slider | min_value(0), max_value(100), starting_value(min_value), step(1), min_label, max_label |
number | min_value, max_value, integer |
text | multiline(true), max_length(1000, 최대 4,000) |
해당 유형의 목록에 없는 키는 검증에 실패합니다. slider 어노테이션 스킴과 달리 세션의 slider는 min_label과 max_label을 받습니다. 슬라이더는 참가자가 슬라이더를 움직인 뒤에만 응답된 것으로 간주하며, 서버는 질문의 선택지나 범위를 벗어난 응답을 거부합니다. Potato는 세션 응답을 user_state.json이 아니라 세션별로 <output_annotation_dir>/scheduling/schedule.sqlite에 저장합니다. 위 구성에서는 한 참가자가 각 프롬프트 질문에 프롬프트마다 한 번씩, 총 35번 응답할 수 있습니다.
알림
각 웨이브, 경험 표집 블록, 델파이 블록은 네 가지 규칙으로 구성된 reminders 목록을 받습니다.
| 규칙 | 메시지가 발송되는 시점 |
|---|---|
{at: open} | 시간 범위가 열릴 때 |
{before_open: 12h} | 시간 범위가 열리기 12시간 전 |
{after_open: 2h} | 시간 범위가 열린 지 2시간 후, 세션에 아직 응답하지 않은 경우 |
{before_close: 12h} | 시간 범위가 닫히기 12시간 전, 세션에 아직 응답하지 않은 경우 |
Potato는 세션이 이미 응답되었거나 닫혔으면 알림을 건너뛰고, 세션이 열린 뒤에는 before_open 알림을 건너뜁니다. 참가자가 등록하기 전에 예정 시각이 지난 알림은 발송되지 않습니다. after_open 또는 before_close 오프셋은 시간 범위보다 짧아야 합니다. 경험 표집은 before_open을 받지 않는데, 그 메시지가 무작위 프롬프트를 미리 알리게 되기 때문입니다.
알림 발송 설정
아래의 부분 블록은 위 구성의 notifications를 대체하며, 이메일과 Prolific 받은편지함으로 메시지를 보내고 프롬프트에는 사용자 지정 텍스트를 사용합니다.
notifications:
base_url: "https://mood-study.example.org"
channels:
- type: log
- type: smtp
host: smtp.example.org
port: 587
security: starttls # starttls, ssl, or none
username_env: POTATO_SMTP_USER
password_env: POTATO_SMTP_PASSWORD
from: "Mood study <mood-study@example.org>"
- type: prolific
token_env: PROLIFIC_API_TOKEN
templates:
prompt:
subject: "{study}: a short survey is open"
body: "Please answer within {time_left}: {link}\n\nStop messages: {stop_link}"
contact:
consent: {question: contact_consent, value: "Yes"}
delete_after_study: true각 채널 유형은 한 번만 나올 수 있습니다.
| 채널 | 전달 대상 | 키와 기본값 |
|---|---|---|
log | 서버 로그와 관리자 페이지이며, 전체 텍스트와 링크가 포함됨. 참가자에게는 아무것도 전달하지 않음 | 없음 |
smtp | 등록 시 입력한 이메일 주소 | host(필수), from(필수), security(starttls), port(587, ssl이면 465), username_env와 password_env(둘 다 지정하거나 둘 다 생략), timeout(20초) |
twilio | 등록 시 입력한 휴대전화 번호로 SMS | account_sid_env(TWILIO_ACCOUNT_SID), auth_token_env(TWILIO_AUTH_TOKEN), from_number(E.164 번호) 또는 messaging_service_sid_env 중 정확히 하나, timeout(20) |
prolific | Prolific 참가자 ID로 지정한 참가자의 Prolific 받은편지함 | token_env(PROLIFIC_API_TOKEN), study_id(미설정 시 참가자의 STUDY_ID), timeout(30) |
Prolific 채널은 Prolific에서 유입된 참가자에게만 닿습니다. Potato는 *_env 키가 지정한 각 변수를 메시지를 보낼 때 읽습니다. YAML에 password, token, account_sid 같은 키로 자격 증명이 들어 있으면 Potato는 시작할 때 멈춥니다. 변수가 없으면 시작할 때 로그에 기록되고, 이후 해당 채널의 각 메시지는 변수 이름과 함께 실패합니다. Potato를 시작하기 전에 서버 환경에 변수를 설정하세요.
export POTATO_SMTP_USER="mood-study@example.org"
export POTATO_SMTP_PASSWORD="<your SMTP password>"
export PROLIFIC_API_TOKEN="<your Prolific API token>"notifications 키 | 기본값 | 의미 |
|---|---|---|
dry_run | false | 모든 채널의 모든 메시지를 생성하고 수신자를 지정하며, 전송하지 않고 각 메시지를 dry_run으로 기록함 |
base_url | http://localhost:<port> | 링크에 사용되는 공개 주소. dry_run이 켜져 있지 않으면 log 이외의 모든 채널에 필수 |
channels | 없음 | 위의 채널들 |
contact.consent | 채널이 있으면 필수 | {question, value}로, 단계 페이지의 동의 질문과 연락을 허용하는 응답 |
contact.delete_after_study | true | 참가자에게 남은 세션이 없어지면 참가자의 연락처를 삭제함 |
templates | 기본 제공 | 종류별 메시지 텍스트 |
템플릿 종류는 before_open, open, reminder(after_open과 before_close용), prompt(경험 표집 프롬프트의 첫 메시지), delphi_round입니다. 각 종류는 subject와 body를 받습니다. 사용할 수 있는 자리 표시자는 {study}, {session}, {opens}, {closes}, {time_left}, {link}, {stop_link}입니다. Potato는 {opens}와 {closes}를 참가자의 시간대로 표시하며, 템플릿이 알 수 없는 자리 표시자를 사용하면 시작할 때 멈춥니다. SMS는 본문만 사용하며 Twilio의 1,600자 제한까지 허용되고, Prolific 메시지는 제목과 본문을 이어 붙입니다.
Potato는 메시지를 보내기 전에 데이터베이스에서 각 메시지를 선점합니다. 따라서 재시작 후나 두 번째 서버 프로세스에서도 같은 메시지를 두 번 보내지 않습니다. 전송 도중 중단된 메시지는 실패로 표시되며 다시 보내지 않습니다. 일시적 오류가 발생하면 세션이 아직 열려 있는 동안 Potato는 5분 뒤, 그다음 10분 뒤에 다시 시도하며, 총 시도는 최대 세 번입니다. 일시적 오류는 SMTP 연결 오류와 Twilio 또는 Prolific의 429 또는 5xx 응답입니다. 다른 오류는 즉시 실패합니다.
스케줄 전체 설정
schedule 키 | 기본값 | 의미 |
|---|---|---|
timezone | UTC | 브라우저가 시간대를 보고하지 않을 때 등록 페이지에 미리 선택되는 IANA 시간대 |
seed | 빈 값 | 무작위 프롬프트 시각과 델파이 코멘트의 순서에 섞이는 텍스트 |
poll_seconds | 30 | 백그라운드 루프가 실행되는 간격, 1초부터 3,600초까지 |
scheduler | true | 서버와 함께 백그라운드 루프를 실행함 |
waves, experience_sampling, delphi | 없음 | 하나 이상 필수 |
백그라운드 루프는 예정된 메시지를 보내고, 델파이 라운드를 열고 닫으며, 완료한 참가자의 연락처를 삭제합니다. scheduler: false이면 관리자가 /admin/schedule에서 Send due messages now를 누르거나 /admin/api/schedule/tick에 POST하기 전까지 아무것도 발송되지 않습니다. schedule 블록 없이 notifications 블록만 두면 검증에 실패합니다.
재개 링크와 연락처
재개 링크(/s/<token>)는 참가자를 로그인시키고 세션 하나를 열며, 세션이 닫히면 만료됩니다. 링크는 POTATO_SCHEDULE_LINK_KEY로 HMAC-SHA256 서명되며, 해당 변수가 설정되지 않았으면 <task_dir>/.potato/scheduling/link.key에 한 번 생성되는 무작위 키로 서명됩니다. 서버가 새 디스크에서 재시작할 수 있다면 이 변수를 설정하세요. 참가자가 메시지를 중단하면 Potato는 그 참가자에게 이전에 보낸 모든 링크를 무효화합니다.
이메일 주소와 전화번호는 어노테이션 출력 디렉터리 밖의 <task_dir>/.potato/scheduling/contacts.sqlite에 파일 모드 0600으로 보관됩니다. 어떤 내보내기 도구, 데이터 아카이브, 배포 번들도 연락처 저장소를 읽지 않으며, 알림 로그에는 채널만 기록되고 주소는 절대 기록되지 않습니다. 재시작할 때마다 디스크를 지우는 호스트에서는 주소가 사라지며, 그 호스트의 참가자는 더 이상 메시지를 받지 못합니다.
참가자는 상태 페이지 또는 모든 메시지에 들어 있는 중단 링크에서 메시지를 중단할 수 있습니다. 중단 링크는 확인 페이지를 열며, 연락처는 해당 페이지의 버튼으로만 삭제됩니다. 따라서 메일 스캐너가 링크를 열어도 아무도 수신 거부되지 않습니다. delete_after_study: true이면 참가자에게 열려 있거나 예정된 세션이 없어질 때 연락처가 삭제되며, 델파이 연구는 패널이 끝날 때까지 연락처를 유지합니다.
놓친 세션과 만료된 세션
세션은 해당 세션의 시간 범위가 끝날 때 닫히며, 서버는 닫힌 뒤에 제출된 응답을 거부합니다. Potato는 닫힌 세션을 세 가지 상태 중 하나로 기록합니다.
- answered는 참가자가 세션이 닫히기 전에 제출했음을 뜻합니다.
- expired는 참가자가 세션을 열었거나 응답을 시작했지만 닫히기 전에 제출하지 않았음을 뜻합니다.
- missed는 세션이 열려 있는 동안 참가자가 세션을 한 번도 열지 않았음을 뜻합니다.
Potato는 닫힌 세션을 다시 예약하지 않으며, 그 세션에 대기 중이던 알림은 건너뜁니다. 오래된 링크를 연 참가자는 세션이 언제 닫혔는지와 다음 세션이 언제 열리는지를 확인하지만, 다음 세션이 무작위 프롬프트라면 시각은 표시되지 않습니다. 이행률은 응답된 세션을 닫힌 세션으로 나눈 값이며, 참가자별 및 세션 종류별로 계산됩니다.
관리자 페이지
/admin/schedule에는 관리자 API 키 또는 RBAC 관리자 역할이 필요합니다. 이 페이지는 각 참가자의 시간대, 동의, 이행률, 응답 지연 시간의 중앙값, 다음 세션을 보여 주고, 모든 메시지를 상태별로 나열하며 log와 dry_run 메시지는 전체 텍스트를 함께 보여 줍니다. 델파이 라운드는 항목 통계와 함께 표시됩니다. 페이지의 버튼은 아래의 POST 엔드포인트에 대응합니다.
| 엔드포인트 | 메서드 | 동작 |
|---|---|---|
/admin/api/schedule | GET | 페이지의 데이터를 주소 없이 JSON으로 반환 |
/admin/api/schedule/tick | POST | 예정된 메시지를 지금 발송 |
/admin/api/schedule/delphi/close_round | POST | 열려 있는 델파이 라운드를 닫음 |
/admin/api/schedule/contact/<participant>/delete | POST | 한 참가자의 연락처를 삭제 |
내보내기
세션 응답은 항목 어노테이션이 아니므로 주 내보내기에는 세션 응답이 포함되지 않습니다. 대신 schedule 내보내기가 보조 테이블을 쓰며, 모든 테이블에는 주 내보내기의 user_id와 같은 ID를 가진 participant 열이 있습니다.
python -m potato.export --config config.yaml --format schedule --output out/| 파일 | 행 |
|---|---|
schedule_sessions.csv | 참가자와 세션당 하나, 그리고 일반 흐름에 대한 참가자당 하나의 enrolment 행 |
schedule_compliance.csv | 참가자와 세션 종류별로, sessions, closed, answered, expired, missed, compliance, median_latency_seconds 포함 |
schedule_notifications.csv | 계획된 모든 메시지이며, session_id, rule, template, channel, due_at_utc, status, attempts, sent_at_utc, detail 포함. 주소와 메시지 텍스트는 없음 |
delphi_ratings.csv | 델파이 전용이며, participant, round, item, rating, comment 포함 |
delphi_rounds.csv | 델파이 전용이며, 라운드와 항목별로 n, median, q1, q3, iqr, mean, median_change, changed_share, settled, opens_at_utc, closed_at_utc, round_status, stop_reason 포함 |
델파이 평가는 delphi_ratings.csv에 기록되고, 라운드 세션은 상태와 응답 지연 시간과 함께 schedule_sessions.csv에 나타납니다. schedule_sessions.csv의 열은 다음과 같습니다.
| 열 | 의미 |
|---|---|
kind | wave, prompt, round, 또는 enrolment |
session_id, wave, day, prompt, round, title | 행이 가리키는 세션. day는 1부터 시작하는 표집일이고 prompt는 그날의 프롬프트 번호 |
timezone | 참가자의 시간대 |
opens_at_utc, closes_at_utc, opens_at_local, closes_at_local | UTC와 현지 시각으로 나타낸 시간 범위 |
status | answered, expired, missed, open, started, 또는 upcoming(등록 행에서는 enrolled) |
prompt_at_utc | 첫 메시지가 발송된 시각이며, 메시지가 발송되지 않았다면 시간 범위의 시작 |
opened_at_utc, first_answer_at_utc, submitted_at_utc | 참가자가 페이지를 연 시각, 양식을 처음 건드린 시각, 제출한 시각 |
latency_seconds | prompt_at_utc부터 첫 응답까지 |
response_seconds | 첫 응답부터 제출까지 |
reminder_channels | 등록 행에서, 참가자가 메시지를 받는 채널 |
answer.<name> | 질문당 한 열이며, 다중 선택 응답은 ;로 연결됨 |
예제
python potato/flask_server.py start examples/advanced/experience-sampling/config.yaml -p 8000이 예제는 위의 표집 계획을 log 채널과 드라이 런 Twilio 채널로 실행합니다. /admin/schedule의 관리자 페이지에는 각 프롬프트의 재개 링크가 나열됩니다. examples/advanced/two-wave-panel/ 폴더와 examples/advanced/delphi-study/ 폴더에는 1주 후속 조사 패널과 3라운드 델파이 연구가 들어 있습니다.
더 읽을거리
- 다단계 워크플로 - 일반 흐름의 동의, 안내, 설문 페이지
- 설문 도구 - 연구 전후 페이지를 위한 검증된 설문지
- Prolific과 SONA에서 크라우드소싱 연구 운영하기 - 참가자 모집과 Prolific에서의 후속 웨이브 운영
- 심리학 - 온라인 행동 실험을 위한 Potato
- 설문 연구 - 자체 서버에서 설문 실험을 하기 위한 Potato
구현 세부 사항은 소스 문서를 참조하세요.